1. git과 sops를 사용한 선언적 패러다임으로 비밀을 관리하기(feat mise)

문제 인식

개발 환경에서의 secrets 설정은 우리가 새로운 프로젝트를 시작할때마다 매번 마주치게 되는 문제입니다.
저는 지금까지 개발 환경을 세팅할때 secrets 환경 변수들을 DM으로 받거나 어딘가에서 복사해서 붙혀넣기를 해서 사용했는데 아래와 같은 문제점들을 겪었어요.

  1. 신규 입사자의 혼란과 끝없는 동기화 문제
    새로운 팀원이 합류할 때마다 "저... .env 파일 어디서 받나요?"라는 질문이 오고 갑니다. 누군가는 오래된 버전을 전달해주기도 하죠. "제 로컬에서만 서버가 안 떠요!"라며 원인 모를 에러를 잡기 위해 소중한 시간을 낭비하게 될 수 있습니다. 팀 전체가 동일한 환경을 공유하고 있다는 확신을 할 수 없습니다.
  2. 보안 문제
    메신저 DM 기록에 API 키와 데이터베이스 비밀번호가 그대로 남아있습니다. 만약 누군가의 메신저 계정이 탈취된다면, 프로젝트의 모든 비밀 정보가 속수무책으로 외부에 노출됩니다.
  3. 추적할 수 없는 변경 이력
    어느 날 갑자기 잘 되던 기능이 동작하지 않습니다. 비밀 키 값이 변경된 것 같습니다. 아무도 변경 이력을 모르기 때문에 전체 채널에 물어봐야 합니다. 누가, 언제, 그리고 "왜" 그 비밀을 변경했는지 알 수 있는 방법이 전혀 없습니다.
    누군가 테스트를 위해 자신의 로컬 .env 파일을 수정했다가, 실수로 잘못된 정보를 다른 팀원에게 전달할 수도 있습니다. 이런 휴먼 에러는 원인을 찾기가 매우 어렵습니다. git처럼 변경 사항을 추적하고 필요시 롤백할 수 있는 체계가 전혀 없습니다.
  4. 자동화의 걸림돌
    CI/CD 파이프라인을 구축할 때 비밀 관리 문제가 복잡해집니다. 비밀 값들을 일일히 복사해서 등록하고, container에 일일히 주입해야 합니다.

만약 비밀번호 파일 자체를 암호화해서 권한 지정과 함께 파일들과 함께 git에 커밋할 수 있다면 어떨까요?
그러면 위의 문제를 전부 해결할 수 있습니다.
gitops랑도 아주 잘어울리죠.
그리하여 찾게된 것이 sops입니다.

sops는 암호(AWS KMS, GCP KMS, Azure Key Vault, age, PGP)를 사용하여 비밀 관리 패턴을 쉽게 적용할 수 있는 암호화 파일 에디터입니다.
기존 환경에서 KMS를 쓰시는 분들도 사용할 수 있습니다. 이 글에서는 간단히 현대화 암호 툴인 age를 사용하겠습니다.

이제 개발 환경을 손쉽게 공유하고 사용할 수 있는 mise를 통해 sops와 age를 설치하고 사용하는 예제를 보여드리겠습니다.


시작하기

그러면 이후 mise 설정 파일이 있는 폴더에 진입시 자동으로 환경 세팅이 됩니다.
Trust them with 'mise trust'. See https://mise.jdx.dev/cli/trust.html for more information.
위 메세지가 뜬다면 mise trust 를 쳐주면 됩니다.

2. 프로젝트 루트의 mise.toml에 다음과 같이 명시합니다.

# projectRoot/mise.toml
[tools.sops]
version = "3.11.0"

[tools.age]
version = "1.2.1"

[env]
_.file = [
	# 프로젝트 내 public 환경 변수
	{ path ="public.local.env"}
]

public.local.env을 만들어줍니다.

ENVIRONMENT=local

제대로 설정이 되었다면 아래와 같이 표시될 것 입니다.

> echo $ENVIRONMENT
local

3. 설치 후 해당 프로젝트 내에서 바로 sops, age를 사용할 수 있습니다.

mise install

하면 sops, age가 설치됩니다.

> sops --version
sops 3.11.0 (latest)

> age-keygen --version
v1.2.1

> which sops
/Users/hj/.local/share/mise/installs/sops/3.11.0/sops

4. age private, public 키 파일을 생성합니다.

저는 xdg base의 기본 위치를 기반으로 한 sops age key 기본 위치~/.config/sops/age/key.txt 에 생성하겠습니다.

mkdir -p ~/.config/sops/age
> age-keygen -o ~/.config/sops/age/key.txt
Public key: age1z7tjf83ntnsutgypde7clluwes3rgh3pde0y34wczzczfkaerchq60c47u

$SOPS_AGE_KEY_FILE 환경 변수에 ~/.config/sops/age/key.txt를 지정하고 (ex: .zshrc or 전역 mise.toml)
그 후 mise.toml에 age 키 설정을 env를 참조하게 합니다.

# mise.toml
# ...

[settings.sops]
age_key_file = "{{env.SOPS_AGE_KEY_FILE}}"

# ...

5. .sops.yaml에 키 등록 및 유저 추가

.sops.yaml 파일은 비밀 키 관리에 대한 권한 및 레시피 설정 파일인데요.
아래와 같이 해당 유저의 public 키를 등록합니다.
path_regex에 해당 유저가 명시되어 있어야 해당 비밀에 접근할 수 있습니다.

저는 암호화된 비밀을 구분하기 위해 파일명에 .enc를 붙힙니다.

.sops.yaml은 code owner 설정을 통해 변경시 무조건 관리자가 검토할 수 있게 해야합니다.

# projectRoot/.sops.yaml
keys:
	# 구성원 추가시 age public key 등록
	- &hj age1z7tjf83ntnsutgypde7clluwes3rgh3pde0y34wczzczfkaerchq60c47u
	- &suhyun age14c6z2hf8qr9lumph8smjh7yny25nd7v4xy9guc8n5r2pfrmhjfasrjmhz5

creation_rules:
	# 공통 
	- path_regex: shared/[^/]+\.enc\.(yaml|json|env|ini)$
		key_groups:
			- age:
				- *hj
			   - *suhyun

	# client
	- path_regex: client/[^/]+\.enc\.(yaml|json|env|ini)$
		key_groups:
			- age:
				- *hj
				- *suhyun
  
	# db 
	- path_regex: db/[^/]+\.enc\.(yaml|json|env|ini)$
		key_groups:
			- age:
				- *hj

6. 비밀 파일 작성

sops를 사용해서 비밀 파일을 작성할 수 있습니다.
$SOPS_EDITOR 또는 $EDITOR 환경 변수로 등록된 에디터를 통해 열려 마치 일반 파일을 작성하듯 작성할 수 있습니다.
생성시 .sops.yaml에서 명시한 패턴과 같아야 합니다. 그렇지 않으면 error loading config: no matching creation rules found 메세지가 노출됩니다.

비밀을 이렇게 생성합니다.

> sops ./shared/test.enc.yaml

그러면 아래와 같은 화면이 뜹니다.
CleanShot 2025-10-06 at 19.32.35@2x 1.jpg

위 화면에서 비밀 값들을 넣고 저장하고 나오면 됩니다. 저는 예시로 아래와 같이 작성했습니다.
CleanShot 2025-10-06 at 19.36.04@2x.jpg

그 다음 해당 파일을 읽어보면

> cat ./shared/test.enc.yaml

아래와 같이 암호화된 것을 볼 수 있습니다.
CleanShot 2025-10-06 at 19.39.43@2x 1.jpg|700

암호화 되어 있고 각자 환경에서의 암호화 키로만 풀 수 있기 때문에 안심하고 git에 커밋하여 형상 관리를 할 수 있습니다.

복호화된 내용을 보려면 -d 옵션을 붙히면 됩니다.

> sops -d ./shared/test.enc.yaml
TEST_SECRETS: secrets!

만약 mise를 통해서 자동으로 해당 workspace 진입시 자동으로 환경 변수를 등록하고 싶다면 아래와 같이 mise.toml의 env에 비밀 파일을 추가하면 자동으로 불러옵니다.

# mise.toml

[env]
_.file = [
	{ path = "public.local.json"},
	{ path = "./shared/test.enc.yaml", redact = true, read_only = true}
]

테스트

> env | grep TEST_SECRETS
TEST_SECRETS=secrets!!

아래와 같이 기존 비밀 파일을 암호화 해서 저장하거나, 확장자 타입을 바꿀수도 있습니다.

use case 1)
기존의 app.env 파일을 암호화하고 싶을 때도 있습니다. 하지만 cat ./app.env | sops encrypt처럼 파이프를 사용하면, sops는 어떤 암호화 규칙(creation_rules)을 적용해야 할지 알 수 없습니다.

이럴 때 --filename-override 옵션을 사용합니다. 이 옵션으로 .sops.yaml에 정의된 path_regex 패턴과 일치하는 가상의 파일 경로를 알려주면, sops가 올바른 암호화 키를 찾아 적용할 수 있습니다.

> cat ./app.env | sops encrypt --filename-override ./client/app.enc.env --output-type yaml --output ./client/app.enc.yaml

> sops -d ./client/app.enc.yaml
MY_APP_NAME: wow_sops

> cat ./client/app.enc.yaml
MY_APP_NAME: ENC[AES256_GCM,data:...]
sops:
	age:
		- recipient: age14231...
	enc: ...

use case 2)

exec-env를 통해서 비밀 파일의 비밀을 참조하며 외부로 노출하지 않고 격리된 환경 속에서 스크립트를 실행할 수도 있습니다.

client/mise.toml
CleanShot 2025-10-06 at 20.22.06@2x.jpg|600

secrets.yaml 내에 있는 $RESOURCE_R2_S3_ACCESS_KEY 를 사용하여 임시 RCLONE 환경변수를 등록하고 rclone을 실행합니다.
실행 후에는 RCLONE 관련 환경 변수가 남아있지 않아 비밀이 노출되지 않습니다.


마무리

keygroup을 통해 그룹을 설정하고 role을 통해 역할 분리와 특정 context 내의 비밀만 접근할 수 있게 하는 등 advanced한 사용 사례에 대해서도 구현되어 있으니
더 자세한 sops의 사용법은 github sops readme에서 읽으시면 됩니다.

CI/CD 환경에서도 마찬가지로 해당 배포 환경의 age 키를 생성하고 .sops.yaml에 키를 등록해주면 됩니다.
test.dev.enc.yaml, test.prod.enc.yaml 같은 배포 환경별 비밀 파일을 생성하여 따라 sops로 풀어 사용하면 됩니다.
그로 인해 배포 환경에서도 비밀에 대해 개발 환경과 동일한 로직과 git 형상관리를 그대로 사용할 수 있습니다.

더 읽어볼 것

[draft] 2. nix flake와 sops-nix를 통한 선언적 시스템 비밀 관리