KMS는 그냥 "키 관리 서비스"라고만 알고 있었다. 그런데 시크릿 조회 권한 요청 몇 건을 처리하다 보니 RAM, STS, AssumeRole, RRSA까지 줄줄이 엮여 나왔다. 헷갈렸던 부분을 기록 해 본다.
KMS가 하는 두 가지 일
| 키 관리 | 암호화 키를 보관하고 암호화·복호화를 수행 | kms:Decrypt, kms:Encrypt |
| 시크릿 관리 | DB 비밀번호 같은 값을 이름으로 저장하고 꺼내 줌 | kms:GetSecretValue |
시크릿은 저장될 때 키 관리 쪽의 키로 암호화된다. 기본 서비스 키면 GetSecretValue만, 커스텀 키면 Decrypt까지 필요하다.
권한 확인은 RAM이 한다
① 앱: AK/SK로 GetSecretValue 요청에 서명
② KMS → RAM: "이 요청자가 이 시크릿을 조회할 수 있나?"
③ RAM: 서명 검증(누구인지) → 붙어 있는 Policy 검사(무엇을 할 수 있는지)
④ KMS: 시크릿을 복호화해서 반환
AK/SK는 "누구인지"를 증명하고, RAM이 "무엇을 할 수 있는지" 판단하고, KMS는 시크릿을 꺼내 준다.
신뢰 정책과 Policy
- Role 신뢰 정책: 누가 이 Role을 쓸 수 있는지
- Policy: 무엇을 할 수 있는지 (시크릿 이름, 복호화 키)
이 둘을 나눠서 보니까 많이 정리됐다.
AssumeRole
잠시 다른 신분(Role)으로 바꿔서 일하는 것이다. 사원증(AK/SK)을 보여주고, 출입 명단(신뢰 정책)에 이름이 있으면 임시 출입증(기본 1시간)을 받는 구조다.
사용자 (AK/SK)
│ AssumeRole
▼
STS ─ 요청자에게 sts:AssumeRole 권한이 있나?
─ Role 신뢰 정책이 이 요청자를 허용하나?
▼
임시 자격증명 (AccessKeyId · AccessKeySecret · SecurityToken)
▼
Role에 붙은 Policy 권한으로만 API 호출
변형도 있다.
| AssumeRole | AK/SK | 사람의 로컬 앱, 다른 계정 접근 |
| AssumeRoleWithOIDC | OIDC 토큰 | RRSA, GitHub Actions 같은 CI |
| AssumeRoleWithSAML | SAML | 회사 SSO 로그인 |
하나 깨달은 점이 있다. 사람이 AK/SK로 AssumeRole하는 구조는 키 유출 방어 효과가 크지 않다. 유출된 키로 공격자도 똑같이 AssumeRole할 수 있기 때문이다. 이 구조에서 Role의 진짜 이점은 "누가 쓸 수 있는지"를 그룹과 분리해 세밀하게 통제한다는 점이다.
RRSA (RAM Roles for Service Accounts)
ACK(쿠버네티스) Pod가 AccessKey 없이 Alibaba 권한을 받는 방법이다. KMS 전용이 아니고, Pod에서 Alibaba API를 호출할 때 일반적으로 쓴다. 이미 쓰고 있던 RRSA Role의 신뢰 정책은 이런 모양이었다.
{
"Version": "1",
"Statement": [
{
"Effect": "Allow",
"Action": "sts:AssumeRole",
"Principal": {
"Federated": ["acs:ram::<계정ID>:oidc-provider/ack-rrsa-<클러스터ID>"]
},
"Condition": {
"StringEquals": {
"oidc:aud": "sts.aliyuncs.com",
"oidc:iss": "https://oidc-ack-<리전>.oss-<리전>.aliyuncs.com/<클러스터ID>"
},
"StringLike": {
"oidc:sub": "system:serviceaccount:my-namespace:*"
}
}
}
]
}
| Principal.Federated | 신뢰할 클러스터 | 금지. 열면 다른 클러스터도 사용 가능 |
| oidc:iss | 토큰 발급자 | 금지 |
| oidc:aud | sts.aliyuncs.com 고정 | 금지 |
| oidc:sub | system:serviceaccount:<네임스페이스>:<SA> | 허용 (StringLike로) |
oidc:sub에서 ServiceAccount는 사람 계정도 Alibaba 계정도 아니고, 쿠버네티스 안에서 Pod가 쓰는 신분이다. 보통 앱 하나에 SA 하나라서 "SA ≈ 애플리케이션"으로 봐도 된다.
| system:serviceaccount:my-ns:backend-api | SA 하나 |
| system:serviceaccount:my-ns:backend-* | 접두사가 같은 SA들 |
| system:serviceaccount:my-ns:* | 네임스페이스 전체 (앞으로 만들 SA 포함) |
| system:serviceaccount:* | 클러스터 전체. 쓰지 말 것 |
네임스페이스 단위(my-ns:*)로 묶는 건 흔한 방식이다. 다만 이렇게 하면 실제 보안 경계는 쿠버네티스 RBAC가 된다. 그 네임스페이스에 배포할 수 있는 사람은 누구나 이 Role의 권한을 얻는다. 여러 네임스페이스를 열어야 하면 와일드카드 대신 배열로 나열하면 된다(OR 조건).
권한 부여 방식 비교
| 누가 쓰나 | 사람 (그룹 전원) | 사람 (지정한 사용자) | ACK 밖의 앱 | ACK Pod |
| 인증 | 본인 AK/SK | AK/SK → 임시 자격증명 | 영구 AK/SK | OIDC 토큰 → 임시 자격증명 |
| Policy 연결 | 그룹 | Role | 사용자 | Role |
| 요청자 쪽 sts 권한 | 불필요 | 필요 | 불필요 | 불필요 |
| 권한 회수 | 그룹에서 해제 (전원) | 신뢰 정책에서 한 줄 삭제 | Policy 해제 | oidc:sub 수정 |
| 적합한 경우 | 개발, 그룹 전원이 쓸 시크릿 | 운영, 특정인만 쓸 시크릿 | VM 등 외부 앱 | ACK 앱 (최우선) |
나는 이 순서로 고르기로 했다. ACK면 RRSA → 사람이면 그룹(개발) 또는 Role(운영) → 그 밖의 앱만 서비스 계정 직접.
앞으로 요청이 오면
- 앱이 어디서 도는지 확인하고 방식을 정한다.
- Policy를 만든다(요청마다 1개). 리전·계정 ID·시크릿 경로를 모두 명시한다. 계정마다 ID가 다르니 이전 요청 값을 복사하지 않도록 주의한다.
- Decrypt는 일단 빼고 테스트한다. 오류가 나면 그 시크릿의 키만 추가한다. *는 쓰지 않는다.
- Role 방식이면 신뢰 정책 기본값(root)을 반드시 교체한다. Role 하나에 여러 요청의 Policy를 모으지 않는다. 모으면 권한이 합쳐져서 모든 대상이 모든 권한을 받게 된다.
- 분석기 경고는 실제 호출로 판단한다.
- Policy·Role 설명란에 요청번호와 연결 대상을 적어 둔다.
테스트가 실패하면 에러에 나온 단어로 원인을 구분한다.
| 에러 메시지 | 확인 해 볼 곳 |
| sts:AssumeRole | 요청자 쪽 sts 권한, Role 신뢰 정책 |
| GetSecretValue, 시크릿 ARN | Policy의 리전·계정 ID·시크릿 이름 |
| Decrypt, key/ | 커스텀 키 → 해당 키로 Decrypt 추가 |