DevOps/Study

CI/CD Study 7주차 Vault (2/2)

juyeon22 2025. 11. 29. 22:54

CI/CD Study 7주차 Vault (2/2)

가시다님이 운영하시는 CI/CD Study 7주차 내용과 유형욱님의 Vault 내용을 정리한 게시글 입니다.

1. Jenkins + Vault (AppRole) - CI

Vault KV Store에 저장한 username, password을 Jenkins을 활용해서 획득해보겠습니다.

기존 실습환경 에서 추가적으로 진행하겠습니다.

Jenkins + KV 시크릿 실습 환경 구성

kubectl create ns jenkins
cat <<EOF | kubectl apply -f -
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
  name: jenkins-pvc
  namespace: jenkins
spec:
  accessModes:
    - ReadWriteOnce
  resources:
    requests:
      storage: 10Gi
---
apiVersion: apps/v1
kind: Deployment
metadata:
  name: jenkins
  namespace: jenkins
spec:
  replicas: 1
  selector:
    matchLabels:
      app: jenkins
  template:
    metadata:
      labels:
        app: jenkins
    spec:
      securityContext:
        fsGroup: 1000
      containers:
        - name: jenkins
          image: jenkins/jenkins:lts
          ports:
            - name: http
              containerPort: 8080
            - name: agent
              containerPort: 50000
          livenessProbe:
            httpGet:
              path: "/login"
              port: 8080
            initialDelaySeconds: 90
            periodSeconds: 10
            timeoutSeconds: 5
            failureThreshold: 5
          readinessProbe:
            httpGet:
              path: "/login"
              port: 8080
            initialDelaySeconds: 60
            periodSeconds: 10
            timeoutSeconds: 5
            failureThreshold: 3
          volumeMounts:
            - name: jenkins-home
              mountPath: /var/jenkins_home
      volumes:
        - name: jenkins-home
          persistentVolumeClaim:
            claimName: jenkins-pvc
---
apiVersion: v1
kind: Service
metadata:
  name: jenkins-svc
  namespace: jenkins
spec:
  type: NodePort
  selector:
    app: jenkins
  ports:
    - nodePort: 30001
      port: 8080
      targetPort: http
      protocol: TCP
      name: http
    - port: 50000
      targetPort: agent
      protocol: TCP
      name: agent
EOF
# 초기 암호 확인
kubectl exec -it -n jenkins deploy/jenkins -- cat /var/jenkins_home/secrets/initialAdminPassword
# a8cb7535dc874d3b839da1928e83037d

cd approle-creds
# role_id 확인
cat role_id.txt
b8654b70-7b97-2ed7-99cd-48ff94ca937d
# secret_id 확인
cat secret_id.txt
630e9122-a9a2-cbe0-8643-6564fffac384

Jenkins 초기 설정은 해당 링크에서 확인 하시면 됩니다. 링크

초기 설정 후 Vault Plugin을 설치 합니다.

Figure 1.1 Vault Plugin 설치

Vault URL: http://vault.vault.svc:8200 입럭 후 add를 클릭 하여 추가합니다.

Figure 1.2 Vault Plugin 설치

  • Kind: Vault AppRole Credential
  • Role ID & Secret ID 입력
  • ID vault-approle-creds

로 지정 후 Add 버튼을 클릭하여 추가합니다.

Figure 1.3 Vault Plugin 설치

Jenkins Pipeline 구성

Jenkinsfile을 이용하여 pipeline을 구성해보겠습니다.

Figure 1.4 pipeline 구성

pipeline {
  agent any

  environment {
    VAULT_ADDR = http://vault.vault.svc:8200
  }

  stages {
    stage('Read Vault Secret') {
      steps {
        withVault([
          vaultSecrets: [
            [
              path: 'secret/sampleapp/config',
              engineVersion: 2,
              secretValues: [
                [envVar: 'USERNAME', vaultKey: 'username'],
                [envVar: 'PASSWORD', vaultKey: 'password']
              ]
            ]
          ],
          configuration: [
            vaultUrl: "${VAULT_ADDR}",
            vaultCredentialId: 'vault-approle-creds'
          ]
        ]) {
          sh '''
            echo "Username from Vault: $USERNAME"
            echo "Password from Vault: $PASSWORD"
          '''
          script {
            echo "Username (env): ${env.USERNAME}"
            echo "Password (env): ${env.PASSWORD}"
          }
        }
      }
    }
  }
}

style="text-align:center;"> Figure 1.5 pipeline 수행 결과

Jenkins + 동적(Dynamic) DB 시크릿 실습 환경 구성

동적 시크릿이란 요청 시 즉시 발급되고, TTL이 지나면 자동으로 사라지는 인증 정보입니다.

cat <<EOF | kubectl apply -f -
apiVersion: apps/v1
kind: Deployment
metadata:
  name: postgres
  namespace: default
spec:
  selector:
    matchLabels:
      app: postgres
  template:
    metadata:
      labels:
        app: postgres
    spec:
      containers:
        - name: postgres
          image: postgres:13
          env:
            - name: POSTGRES_PASSWORD
              value: "rootpassword"
            - name: POSTGRES_DB
              value: "mydb"
          ports:
            - containerPort: 5432
---
apiVersion: v1
kind: Service
metadata:
  name: postgres
  namespace: default
spec:
  type: NodePort
  ports:
    - port: 5432
      targetPort: 5432
      nodePort: 30002  # [External] Jenkins 접속용
  selector:
    app: postgres
EOF

# (로컬 터미널에서 수행)
export VAULT_ADDR=http://127.0.0.1:30000
export VAULT_TOKEN=root


# 1. Database Secret Engine 활성화
vault secrets enable database
vault secrets list

# 2. Vault -> Postgres 연결 설정
# (Vault와 DB는 같은 K8s 안에 있으므로 내부 DNS 사용)
vault write database/config/my-postgresql-database \
    plugin_name=postgresql-database-plugin \
    allowed_roles="jenkins-role" \
    connection_url="postgresql://{{username}}:{{password}}@postgres.default.svc.cluster.local:5432/mydb?sslmode=disable" \
    username="postgres" \
    password="rootpassword"

# Success! Data written to: database/config/my-postgresql-database


# 3. "4시간짜리 임시 계정" 생성 규칙 정의 (DB Role)
# 주의: Vault 내부의 Role이 아니라, DB 엔진용 Role입니다.
vault write database/roles/jenkins-role \
    db_name=my-postgresql-database \
    creation_statements="CREATE ROLE \"{{name}}\" WITH LOGIN PASSWORD '{{password}}' VALID UNTIL '{{expiration}}'; GRANT SELECT ON ALL TABLES IN SCHEMA public TO \"{{name}}\";" \
    default_ttl="4h" \
    max_ttl="24h"

# Success! Data written to: database/roles/jenkins-role

# 기존 KV 권한 + 새로운 DB 권한(database/creds/jenkins-role) 병합
vault policy write sampleapp-policy - <<EOF
# 1. KV v2 데이터 읽기
path "secret/data/sampleapp/*" {
  capabilities = ["read"]
}
# 2. KV v2 목록 조회 (플러그인 에러 방지용 필수!)
path "secret/metadata/sampleapp/*" {
  capabilities = ["list", "read"]
}
# 3. DB Creds 발급
path "database/creds/jenkins-role" {
  capabilities = ["read"]
}
EOF

# Success! Uploaded policy: sampleapp-policy

# 관리자 로그인 상태에서 실행. token polices에 default도 함께추가
vault write auth/approle/role/sampleapp-role \
  token_policies="default,sampleapp-policy" \
  secret_id_ttl="0" \
  token_ttl="4h" \
  token_max_ttl="4h"

# Success! Data written to: auth/approle/role/sampleapp-role

# Jenkinsfile

pipeline {
  agent any

  environment {
    VAULT_ADDR = 'http://vault.vault.svc:8200'    
    DB_HOST = 'postgres.default.svc'
    DB_PORT = '5432'
  }

  stages {
    stage('Vault 통합 및 DB 접속 테스트') {
      steps {
        withVault([
          configuration: [
            vaultUrl: "${VAULT_ADDR}",
            vaultCredentialId: 'vault-approle-creds',
            skipSslVerification: true
          ],
          vaultSecrets: [
            // 1. KV Secret (정적 시크릿)
            // KV v2 엔진을 사용하므로 engineVersion: 2를 명시합니다.
            [
              path: 'secret/sampleapp/config',
              engineVersion: 2,
              secretValues: [
                [envVar: 'STATIC_USER', vaultKey: 'username']
              ]
            ],
            // 2. Database Secret (동적 시크릿)
            // DB 엔진은 기본 방식(v1)으로 통신해야 경로 에러가 없습니다.
            [
              path: 'database/creds/jenkins-role',
              engineVersion: 1,
              secretValues: [
                [envVar: 'DB_USER', vaultKey: 'username'],
                [envVar: 'DB_PASS', vaultKey: 'password']
              ]
            ]
          ]
        ]) {
          script {
            echo "=================================================="
            echo "             Vault 연동 테스트 시작                "
            echo "=================================================="

            sh '''
              echo "[1] KV Secret (Static)"
              echo " - 원본 값은 보안상 **** 로 표시됩니다."
              echo " - 실제 값 확인: $(echo $STATIC_USER | sed "s/./& /g")"
            '''

            sh '''
              echo "--------------------------------------------------"
              echo "[2] Database Secret (Dynamic)"
              echo " - Vault가 생성한 임시 계정 ID입니다."
              echo " - 실제 값 확인: $(echo $DB_USER | sed "s/./& /g")"
              echo "--------------------------------------------------"
            '''

            sh '''
              echo "[3] DB Connection Simulation"
              echo " - Connecting to: ${DB_HOST}:${DB_PORT}"
              echo " - User: ${DB_USER}"
              echo " - Password: (Hidden)"
              echo " >> ✅ DB 접속 테스트 성공! (가상)"
            '''
          }
        }
      }
    }
  }

  post {
    success {
      script {
        echo "🎉 Pipeline 성공!" 
        echo "   -> 확인된 DB 계정(${env.DB_USER})은 Vault의 TTL 설정에 따라 1시간 후 자동 삭제됩니다."
      }
    }
    failure {
      echo "💥 Pipeline 실패! Vault 로그나 네트워크 설정을 확인하세요."
    }
  }
}

style="text-align:center;"> Figure 1.5 pipeline 수행 결과

2. Vault Transit Engine

Vault가 제공하는 Encryption as a Service (EaaS) 기능을 Transit Engine을 중심으로 실습을 통해 알아보겠습니다.

✅ Transit Engine

Transit Engine이란 애플리케이션은 데이터를 갖고 있고, Vault는 암호화/복호화/서명 연산과 키 관리만 담당합니다.

Vault는 입력된 Plaintext를 절대 저장하지 않고 암호화 결과(ciphertext)만 반환하며, 키는 Vault 내부에서만 보호됩니다.

내부적으로 AES-256-GCM 기반 대칭키 암호화 사용합니다.

style="text-align:center;"> Figure 2.1 Transit의 기본 동작구조

실습

export NS=vault-demo
export IMAGE=hyungwookhub/vault-transit-demo:v1
export VAULT_ADDR=http://localhost:30000
export VAULT_TOKEN=root

# Vault 서버 현재 상태 확인 (Seal 여부, 클러스터 상태 등)
vault status

# Transit 엔진 활성화 (암복호화 전용 엔진)
vault secrets enable transit
# Success! Enabled the transit secrets engine at: transit/

# ds-poc 이름의 암호화 키 생성 (AES-256-GCM 모드 사용)
vault write -f transit/keys/ds-poc type=aes256-gcm96

# Transit 엔진 내에 생성된 키 목록 확인
vault list transit/keys
# Keys
# ----
# ds-poc



# 암/복호 테스트 (복붙)
PLAINTEXT="My Data"

# 평문을 Base64 → Vault Encrypt API 호출 → ciphertext 출력값 저장
CIPHERTEXT=$(vault write -field=ciphertext transit/encrypt/ds-poc \
  plaintext=$(echo -n "$PLAINTEXT" | base64))

# 암호문을 Vault Decrypt API에 전달 → base64-decoding → 원문 출력
vault write -field=plaintext transit/decrypt/ds-poc \
  ciphertext="$CIPHERTEXT" | base64 -d && echo

# My Data



kubectl create ns $NS

cat <<EOF | kubectl apply -f -
apiVersion: v1
kind: Namespace
metadata:
  name: ${NS}
---
apiVersion: apps/v1
kind: Deployment
metadata:
  name: mysql
  namespace: ${NS}
spec:
  selector:
    matchLabels:
      app: mysql
  template:
    metadata:
      labels:
        app: mysql
    spec:
      containers:
      - name: mysql
        image: mysql:8.0.31
        env:
        - name: MYSQL_ROOT_PASSWORD
          value: "rootpassword"
        ports:
        - containerPort: 3306
---
apiVersion: v1
kind: Service
metadata:
  name: mysql
  namespace: ${NS}
spec:
  type: NodePort
  selector:
    app: mysql
  ports:
  - name: mysql
    port: 3306
    targetPort: 3306
    nodePort: 30002
---
EOF

# DB 생성/계정 (root 사용; 필요 시 app/password 생성)
kubectl -n ${NS} exec -it deploy/mysql -- \
  mysql -uroot -prootpassword -e "CREATE DATABASE IF NOT EXISTS VaultData;"

# Database 생성확인
kubectl -n ${NS} exec -it deploy/mysql -- \
  mysql -uroot -prootpassword -e "SHOW DATABASES LIKE 'VaultData';"

cat <<EOF | kubectl apply -f -
apiVersion: apps/v1
kind: Deployment
metadata:
  name: vault-transit-demo
  namespace: ${NS}
spec:
  replicas: 1
  selector:
    matchLabels:
      app: vault-transit-demo
  template:
    metadata:
      labels:
        app: vault-transit-demo
    spec:
      containers:
      - name: app
        image: ${IMAGE}
        imagePullPolicy: IfNotPresent
        ports:
        - containerPort: 8080
        env:
        - name: MYSQL_HOST
          value: mysql.${NS}.svc.cluster.local
        - name: MYSQL_PORT
          value: "3306"
        - name: MYSQL_DB_NAME
          value: VaultData
        - name: MYSQL_USERNAME
          value: root
        - name: MYSQL_USERPW
          value: rootpassword
        - name: VAULT_HOST
          value: vault.vault.svc.cluster.local    # 필요 시 노드 IP/NodePort로 교체
        - name: VAULT_PORT
          value: "8200"                           # NodePort로 붙을 땐 30000 등으로 교체
        - name: VAULT_SCHEME
          value: http
        - name: VAULT_TOKEN
          value: root
        - name: VAULT_TRANSIT_KEY_NAME
          value: ds-poc
        - name: SERVER_PORT
          value: "8080"
        - name: AWS_REGION
          value: "ap-northeast-2"
---
apiVersion: v1
kind: Service
metadata:
  name: vault-transit-demo
  namespace: ${NS}
spec:
  type: NodePort
  selector:
    app: vault-transit-demo
  ports:
  - port: 8080
    targetPort: 8080
    nodePort: 30003
    name: http
EOF

#  암호문 저장 여부 확인
kubectl -n ${NS} exec -it deploy/mysql -- \
mysql -uroot -prootpassword -e "USE VaultData; SHOW TABLES; SELECT id,data,date_created FROM vault_data;"
mysql: [Warning] Using a password on the command line interface can be insecure.
+---------------------+
| Tables_in_VaultData |
+---------------------+
| vault_data          |
+---------------------+
+----+-----------------------------------------------------------+----------------------------+
| id | data                                                      | date_created               |
+----+-----------------------------------------------------------+----------------------------+
|  1 | vault:v1:AQy+Z4N430yv9xXLFELfB9+JJqCvc/ZwuQv9zwi7ttL7iHh+ | 2025-11-29 22:47:52.685000 |
+----+-----------------------------------------------------------+----------------------------+

kubectl delete ns ${NS}


style="text-align:center;"> Figure 2.2 평문,암호화 데이터 확인

'DevOps > Study' 카테고리의 다른 글

CI/CD Study 8주차 Vault (2/2)  (0) 2025.12.13
CI/CD Study 8주차 Vault (1/2)  (0) 2025.12.13
CI/CD Study 7주차 Vault (1/2)  (0) 2025.11.29
CI/CD Study 6주차 ArgoCD 3/3 (1/2)  (0) 2025.11.23
CI/CD Study 5주차 ArgoCD 2/3  (1) 2025.11.16