DigitalOcean에서 GitOps 구축하기 (3) - SOPS + age로 시크릿 암호화

2025년 12월 04일

devops

시리즈DigitalOcean에서 GitOps 구축하기#3
# SOPS# age# Kubernetes# GitOps# Security

시리즈 소개

시리즈 구성

  1. 왜 GitOps인가?
  2. Kubernetes 클러스터와 Traefik
  3. ExternalDNS - 자동 DNS 레코드 관리
  4. SOPS + age - 시크릿 암호화 (현재 글)
  5. ArgoCD - GitOps 배포 자동화

문제: GitOps에서 시크릿은 어떻게 관리할까?

GitOps의 원칙은 “모든 것을 Git에 저장”하는 것입니다. 하지만 API 토큰, 데이터베이스 비밀번호 같은 민감한 정보를 평문으로 Git에 올릴 수는 없습니다.

🚨

평문 시크릿을 Git에 커밋하면 히스토리에서 완전히 삭제하기 매우 어렵습니다. 한 번 노출되면 해당 시크릿은 즉시 교체해야 합니다.

시크릿 관리 방법들

방법장점단점
Vault기능 풍부, 동적 시크릿복잡함, 별도 인프라 필요
Sealed Secrets클러스터별 암호화클러스터 의존적
External Secrets외부 시크릿 저장소 연동외부 서비스 의존
SOPS + age단순함, Git 친화적수동 복호화 필요

저희는 SOPS + age 조합을 선택했습니다. 이유는:

  • 별도 인프라 불필요
  • Helmfile과 자연스럽게 통합
  • 로컬에서 바로 테스트 가능
  • 팀원끼리 공개키만 공유하면 됨
💡

소규모 팀이라면 SOPS + age가 최적의 선택입니다. Vault는 강력하지만 운영 부담이 크고, Sealed Secrets는 클러스터 마이그레이션 시 문제가 됩니다.


SOPS와 age 소개

SOPS (Secrets OPerationS)

Mozilla에서 만든 암호화 도구로, YAML/JSON 파일의 특정 필드만 암호화할 수 있습니다.

❌ Before
apiVersion: v1
kind: Secret
data:
  password: c3VwZXJzZWNyZXQ=
✅ After
apiVersion: v1
kind: Secret
data:
  password: ENC[AES256_GCM,data:...,iv:...,tag:...]
sops:
  age:
    - recipient: age1...
  lastmodified: "2024-01-01T00:00:00Z"
data 값만 암호화되고, 키 이름과 구조는 유지됩니다

핵심 특징:

  • data 값만 암호화, 키 이름은 그대로 유지
  • 구조 파악 가능 (어떤 시크릿이 있는지 확인)
  • Git diff가 의미있음

age

현대적인 파일 암호화 도구로, GPG의 대안입니다. 간단한 사용법과 보안이 강점입니다.

# 키 생성 (한 줄)
age-keygen -o key.txt

# 암호화
age -r age1... -o secret.enc secret.txt

# 복호화
age -d -i key.txt secret.enc

설치 및 설정

1. 설치

brew install sops age

2. age 키 생성

# 키 저장 디렉토리 생성
mkdir -p ~/.sops

# 키 생성
age-keygen -o ~/.sops/key.txt

생성된 파일 내용:

# created: 2024-01-01T00:00:00+09:00
# public key: age1rklrkck56y95lx83j068p0us0cscutyae7j4ecsh09zwxfje4gvqe2q5ys
AGE-SECRET-KEY-1XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX
🚨

AGE-SECRET-KEY로 시작하는 비밀키는 절대 공유하지 마세요! 이 키가 노출되면 모든 암호화된 시크릿을 복호화할 수 있습니다.

⚠️

키 파일은 반드시 백업해두세요. 키를 잃어버리면 암호화된 시크릿을 복구할 수 없습니다. 안전한 장소(1Password, 물리적 백업 등)에 보관하세요.

3. 환경 변수 설정

# ~/.zshrc 또는 ~/.bashrc에 추가
export SOPS_AGE_KEY_FILE=$HOME/.sops/key.txt

# 적용
source ~/.zshrc

4. .sops.yaml 설정

프로젝트 루트에 .sops.yaml 파일을 생성합니다:

# .sops.yaml
creation_rules:
  # Secret이 포함된 파일명 패턴
  - path_regex: ".*[sS]ecret.*\\.yaml$"
    # 암호화할 YAML 키 (data, stringData만 암호화)
    encrypted_regex: ^(data|stringData)$
    # 복호화할 수 있는 공개키 목록 (팀원 추가 가능)
    age: >-
      age1rklrkck56y95lx83j068p0us0cscutyae7j4ecsh09zwxfje4gvqe2q5ys,
      age19nw8qsqy6zsvq3ydfx87nxuzmw4w6xm96g8tdvpj9t7ph0qe0y4s7zh3l3

사용 방법

시크릿 파일 생성

# secrets/kohi-core-api-secrets.yaml (암호화 전)
apiVersion: v1
kind: Secret
metadata:
  name: kohi-core-api-secrets
  namespace: application-secrets
type: Opaque
data:
  DB_PASSWORD: c3VwZXJzZWNyZXQ=
  API_KEY: YWJjZGVmMTIzNDU2
stringData:
  REDIS_URL: redis://redis:6379

암호화

# 파일 암호화 (in-place)
sops -e -i secrets/kohi-core-api-secrets.yaml

복호화 및 적용

# 복호화 후 kubectl에 파이프
sops -d secrets/kohi-core-api-secrets.yaml | kubectl apply -f -

편집

# SOPS로 직접 편집 (자동으로 복호화 → 편집 → 암호화)
sops secrets/kohi-core-api-secrets.yaml
💡

sops 명령으로 파일을 열면 기본 에디터에서 복호화된 상태로 편집할 수 있습니다. 저장하면 자동으로 다시 암호화됩니다.


Helmfile과 통합

Helmfile은 SOPS를 기본 지원합니다. secrets: 항목으로 지정하면 자동으로 복호화합니다.

디렉토리 구조

infra/
├── helmfiles/
│   ├── .sops.yaml
│   └── releases/
│       ├── helmfile.yaml
│       └── values/
│           └── kohi-core-api-secrets/
│               ├── common.yaml
│               ├── dev.yaml
│               └── secrets/
│                   └── secrets.dev.yaml  # 암호화된 파일

helmfile.yaml 설정

# helmfile.yaml
releases:
  - name: kohi-core-api-secrets
    namespace: application-secrets
    createNamespace: true
    chart: ./charts/kohi-core-api-secrets
    values:
      - ./values/kohi-core-api-secrets/common.yaml
      - ./values/kohi-core-api-secrets/{{ .Environment.Name }}.yaml
    # SOPS로 암호화된 파일 (자동 복호화)
    secrets:
      - ./values/kohi-core-api-secrets/secrets/secrets.{{ .Environment.Name }}.yaml
    missingFileHandler: Warn

배포

# Helmfile이 자동으로 secrets 파일 복호화
helmfile -e dev sync
ℹ️

Helmfile의 secrets: 블록에 지정된 파일은 배포 시 자동으로 복호화됩니다. 별도의 sops 명령어 없이 바로 사용할 수 있습니다.


팀원 추가

새 팀원이 시크릿에 접근해야 할 때:

1. 새 팀원이 age 키 생성

age-keygen -o ~/.sops/key.txt
# public key: age19nw8qsqy6zsvq3ydfx87nxuzmw4w6xm96g8tdvpj9t7ph0qe0y4s7zh3l3

2. 공개키를 .sops.yaml에 추가

# .sops.yaml
creation_rules:
  - path_regex: ".*[sS]ecret.*\\.yaml$"
    encrypted_regex: ^(data|stringData)$
    age: >-
      age1rklrkck56y95lx83j068p0us0cscutyae7j4ecsh09zwxfje4gvqe2q5ys,
      age19nw8qsqy6zsvq3ydfx87nxuzmw4w6xm96g8tdvpj9t7ph0qe0y4s7zh3l3

3. 기존 시크릿 재암호화

# 모든 시크릿 파일 재암호화 (새 공개키 추가)
sops updatekeys secrets/kohi-core-api-secrets.yaml

이제 새 팀원도 자신의 비밀키로 복호화할 수 있습니다.

⚠️

팀원이 퇴사하면 반드시 해당 공개키를 .sops.yaml에서 제거하고, 모든 시크릿을 updatekeys로 재암호화하세요.


실제 사용 예시

Docker Hub 자격 증명

# values/dockerhub-secrets/secrets/secrets.dev.yaml
dockerconfigjson: ENC[AES256_GCM,data:{"auths":{"https://index.docker.io/v1/":{...}}},iv:...,tag:...]

애플리케이션 시크릿

# values/kohi-core-api-secrets/secrets/secrets.dev.yaml
DB_HOST: ENC[AES256_GCM,data:...,tag:...]
DB_PASSWORD: ENC[AES256_GCM,data:...,tag:...]
REDIS_PASSWORD: ENC[AES256_GCM,data:...,tag:...]
JWT_SECRET: ENC[AES256_GCM,data:...,tag:...]

베스트 프랙티스

1. .gitignore 설정

# 비밀키는 절대 커밋하지 않음
*.key
key.txt

# 복호화된 파일 (실수 방지)
*.dec.yaml

2. 파일명 규칙

# 암호화 대상임을 명확히
secrets.dev.yaml     ✅
secret.prod.yaml     ✅
kohi-secrets.yaml    ✅

# 일반 values 파일
common.yaml          (암호화 안됨)
dev.yaml             (암호화 안됨)

3. CI/CD에서 사용

GitHub Actions 예시:

- name: Setup SOPS
  run: |
    echo "${{ secrets.SOPS_AGE_KEY }}" > /tmp/key.txt
    echo "SOPS_AGE_KEY_FILE=/tmp/key.txt" >> $GITHUB_ENV

- name: Deploy
  run: helmfile -e prod sync
🚨

CI/CD 환경에서 age 키를 사용할 때는 반드시 GitHub Secrets 등 안전한 저장소를 사용하세요. 로그에 키가 노출되지 않도록 주의하세요.


트러블슈팅


다음 글 예고

다음 글에서는 ArgoCD를 설치하고 ApplicationSet을 사용하여 Git Push만으로 자동 배포되는 환경을 구축합니다. manifest 저장소에 있는 Helm Chart가 클러스터에 자동으로 동기화됩니다.


참고 자료

© 2025, 미나리와 함께 만들었음