Argo CD는 Git repository의 선언을 cluster에 반영할 수 있다. application을 조회하는 권한과 sync·override·delete 권한의 무게가 다른 이유다. 접근 제어를 단순히 “로그인만 붙이는 작업”으로 보면 인증 뒤에 과도한 권한이 남기 쉽다.
1장 GitOps와 Kubernetes, 2장 Argo CD 시작, 3장 Argo CD 운영에 이어 이번에는 다음 네 층을 나눠 정리한다.
- Authentication: local account 또는 SSO로 사용자가 누구인지 확인한다.
- Authorization: global RBAC와 AppProject role로 가능한 action을 제한한다.
- Credential lifecycle: password·token·client secret의 발급, 만료와 폐기를 관리한다.
- Audit·recovery: 누가 무엇을 했는지 남기고 SSO 장애 때의 복구 경로를 준비한다.
아래 manifest와 command의 domain·account·secret 값은 설명용 placeholder다. 그대로 붙여 넣지 않고 현재 설치한 Argo CD·Keycloak version의 문서와 조직 정책에 맞춰 검증한다.
기본 admin은 bootstrap 계정으로만 쓴다
Argo CD의 built-in admin은 초기 설치와 복구를 위한 강한 계정이다. 초기 password는 argocd-initial-admin-secret에 생성되지만 이를 장기 운영 계정으로 두면 accountability와 최소 권한이 약해진다.
안전한 순서는 다음과 같다.
- 초기 password를 log·문서·shell history에 남기지 않는 통제된 terminal에서 확인한다.
- SSO 또는 별도 break-glass account와 RBAC를 먼저 구성한다.
- 새 경로에서 login·권한·복구 절차를 실제로 test한다.
admin.enabled: "false"로 built-in admin을 비활성화한다.- 비상 접근 credential의 보관자, 승인과 정기 test를 문서화한다.
apiVersion: v1
kind: ConfigMap
metadata:
name: argocd-cm
namespace: argocd
data:
admin.enabled: "false"
SSO가 아직 검증되지 않았는데 admin부터 끄면 스스로 접근을 잃을 수 있다. GitOps로 이 설정을 관리하더라도 적용 전 session과 rollback 경로를 확보해야 한다.
local account는 사람과 automation을 분리한다
Argo CD local account capability는 크게 두 가지다.
login: UI·CLI login에 사용apiKey: API token 발급에 사용
apiVersion: v1
kind: ConfigMap
metadata:
name: argocd-cm
namespace: argocd
data:
accounts.platform-operator: login
accounts.ci-deployer: apiKey
이 선언만으로 권한이 생기는 것은 아니다. local account도 RBAC policy가 필요하고, 일치하는 policy가 없으면 policy.default의 권한을 받는다.
사람에게는 가능한 한 SSO를 사용해 입사·이동·퇴사 lifecycle을 identity provider와 연결한다. local login account는 break-glass처럼 목적과 소유자를 좁힌다. automation account에는 UI login capability를 함께 주지 않고, project 범위 token으로 대체할 수 있는지 먼저 본다.
password나 token을 YAML, Git, CI log, wiki와 command history에 넣지 않는다. credential은 secret manager에서 주입하고 만료·rotation·revocation owner를 정한다.
policy.default를 role:readonly로 두면 왜 위험한가
Argo CD에는 built-in role:readonly와 role:admin이 있다. 이름만 보면 role:readonly를 default로 두는 것이 안전해 보이지만 공식 RBAC 문서는 그렇게 권장하지 않는다.
모든 authenticated user는 policy.default의 권한을 받는다. 더 중요한 점은 default role에서 허용된 권한은 다른 policy의 deny로 다시 제거할 수 없다는 것이다.
apiVersion: v1
kind: ConfigMap
metadata:
name: argocd-rbac-cm
namespace: argocd
data:
policy.default: role:authenticated
policy.matchMode: glob
scopes: '[groups, email]'
policy.csv: |
p, role:platform-readonly, projects, get, sample-apps, allow
p, role:platform-readonly, applications, get, sample-apps/*, allow
g, /platform-readers, role:platform-readonly
이 예시의 role:authenticated에는 의도적으로 p rule을 부여하지 않았다. 모든 login user에게 application 전체 read 권한을 열지 않고, /platform-readers group만 sample-apps project를 읽게 한다.
Argo CD UI가 보이게 하려고 default permission을 넓히기보다 group별로 필요한 project·application read를 명시한다. baseline 권한이 정말 필요하다면 role:authenticated에 최소 action만 추가하고, 모든 authenticated user에게 영구히 허용된다는 전제로 검토한다.
RBAC policy를 읽는 법
Argo CD policy의 기본 형태는 다음과 같다.
p, subject, resource, action, object, effect
g, user-or-group, role
application object는 보통 <project>/<application> 형식이다.
p, role:platform-syncer, applications, get, sample-apps/*, allow
p, role:platform-syncer, applications, sync, sample-apps/*, allow
g, /platform-deployers, role:platform-syncer
*를 편하게 쓰기 시작하면 project 경계가 사라지기 쉽다. 특히 applications, applicationsets, clusters, repositories, projects, accounts는 resource별 영향 범위가 다르므로 한 role에 묶어 포괄 허용하지 않는다.
glob match mode에서 slash가 일반적인 path separator처럼 특별 취급된다고 가정해서도 안 된다. subresource까지 포함하는 action pattern은 설치 version의 RBAC 문서와 실제 can-i 결과로 검증한다.
권한 변경 전후에는 다음 관점의 test를 남긴다.
- 허용해야 하는 action이 성공하는가?
- 허용하지 않은 다른 project의 같은 action이 실패하는가?
- group이 없는 authenticated user가 baseline 이상을 얻지 않는가?
deny가 default allow를 되돌릴 것이라고 잘못 기대하지 않았는가?- application delete, override, exec, logs처럼 민감한 subresource가 따로 열리지 않았는가?
AppProject가 deployment 범위를 제한한다
global RBAC가 “누가 무엇을 할 수 있는가”를 정한다면 AppProject는 “이 project의 application이 어디에서 무엇을 배포할 수 있는가”를 제한한다.
apiVersion: argoproj.io/v1alpha1
kind: AppProject
metadata:
name: sample-apps
namespace: argocd
spec:
description: Sample application delivery boundary
sourceRepos:
- https://github.com/example-org/sample-apps-config.git
destinations:
- server: https://kubernetes.default.svc
namespace: sample-apps
namespaceResourceWhitelist:
- group: apps
kind: Deployment
- group: ""
kind: Service
- group: ""
kind: ConfigMap
roles:
- name: ci-deployer
description: Sync only applications in this project
policies:
- p, proj:sample-apps:ci-deployer, applications, get, sample-apps/*, allow
- p, proj:sample-apps:ci-deployer, applications, sync, sample-apps/*, allow
sourceRepos: ['*'], 모든 cluster·namespace destination, 모든 cluster-scoped resource 허용을 동시에 열면 AppProject의 방어 가치가 크게 줄어든다.
다음을 project별로 구체화한다.
- 허용할 Git repository URL
- 배포 가능한 cluster와 namespace
- 허용·차단할 namespace-scoped, cluster-scoped kind
- orphaned resource와 signature policy
- project role이 다룰 application pattern
default project도 처음에는 넓은 권한을 가질 수 있으므로 production application을 목적별 AppProject로 옮기고 default project의 source·destination을 제한하는 방안을 검토한다.
project role token은 범위와 만료를 함께 둔다
CI가 한 project의 application만 sync해야 한다면 global local account token보다 AppProject role token이 더 좁은 경계를 만들 수 있다.
argocd proj role create-token \
sample-apps \
ci-deployer \
--expires-in 24h
공식 CLI의 default는 만료 없음이므로 --expires-in을 의도적으로 지정한다. 발급 결과는 생성 시 한 번만 확인할 수 있다. terminal scrollback, CI output와 ticket에 복사하지 말고 곧바로 secret manager의 보호된 field에 저장한다.
token 발급은 일상적인 developer 계정에 broad projects, update나 accounts, update 권한을 주기 위한 이유가 아니다. 제한된 bootstrap·platform process가 발급하고 CI에는 완성된 short-lived token만 전달하는 구조가 낫다.
운영 항목도 함께 정한다.
- owner와 사용 pipeline
- 허용 project·action
- 최대 만료 기간
- 자동 rotation 시점
- 유출·퇴사·repository 이전 때의 revoke 절차
- 사용되지 않는 token을 찾는 정기 review
Keycloak 연동 전 lab과 production을 구분한다
Keycloak의 start-dev는 개발 편의를 위한 mode다. Keycloak 공식 container 문서도 insecure default 때문에 production에서 피하라고 명시한다.
격리된 local lab에서는 다음처럼 loopback에만 bind해 동작을 확인할 수 있다.
keycloak_image_ref='quay.io/keycloak/keycloak:PINNED_VERSION'
docker run --rm --name keycloak-lab \
-p 127.0.0.1:8080:8080 \
-e KC_BOOTSTRAP_ADMIN_USERNAME='<LAB_ADMIN_USERNAME>' \
-e KC_BOOTSTRAP_ADMIN_PASSWORD='<LAB_ADMIN_PASSWORD_FROM_SECRET_STORE>' \
"$keycloak_image_ref" \
start-dev
angle bracket 값은 실제 credential이 아니라 placeholder다. command line environment에 secret을 직접 넣으면 process inspection이나 shell history에 남을 수 있으므로 실제 lab에서도 local secret file·runtime secret injection을 사용한다.
production은 start-dev, embedded development database와 plain HTTP를 기반으로 만들지 않는다.
- version tag 또는 image digest 고정
- optimized image build와
start --optimized - TLS와 정확한 hostname 설정
- 지원되는 external database와 backup
- bootstrap credential 제거·rotation
- reverse proxy header와 trusted proxy 범위
- health·metrics·log와 upgrade rehearsal
- secret manager를 통한 DB password·client secret 주입
예전 Keycloak 예제의 admin environment 변수나 오래된 image tag를 그대로 복사하지 말고, 현재 container guide의 KC_BOOTSTRAP_ADMIN_USERNAME·KC_BOOTSTRAP_ADMIN_PASSWORD와 설치 version을 확인한다.
Keycloak client는 redirect URI를 정확히 제한한다
Argo CD와 Keycloak을 direct OIDC로 연결한다면 Keycloak client의 redirect URI를 다음처럼 정확히 등록한다.
https://argocd.example.com/auth/callback
wildcard redirect URI와 web origin은 local test 편의로도 신중해야 하며 production에서는 exact value를 쓴다. client type, confidential client 설정, allowed origin과 logout redirect도 실제 Argo CD URL에 맞춘다.
group claim이 필요하면 Keycloak mapper와 client scope를 명시한다. realm role이나 group이 자동으로 원하는 groups claim에 들어온다고 가정하지 않는다. 실제 ID token의 claim 이름과 값 형식을 확인한 뒤 RBAC group 문자열을 정확히 맞춘다.
client secret은 argocd-secret에서 참조한다
Argo CD 공식 Keycloak 문서는 OIDC client secret을 argocd-secret에 두고 argocd-cm에서는 $ reference로 읽는 pattern을 안내한다.
다음은 key 이름을 설명하는 template이며 실제 secret 값을 Git에 commit해서는 안 된다.
apiVersion: v1
kind: Secret
metadata:
name: argocd-secret
namespace: argocd
type: Opaque
stringData:
oidc.keycloak.clientSecret: <INJECT_FROM_SECRET_MANAGER>
실제 cluster에는 External Secrets, Secrets Store CSI Driver, sealed workflow 등 조직이 승인한 방식으로 값을 주입한다. placeholder를 실제 값처럼 commit하지 않는다.
argocd-cm의 OIDC 설정은 secret reference만 가진다.
apiVersion: v1
kind: ConfigMap
metadata:
name: argocd-cm
namespace: argocd
data:
url: https://argocd.example.com
oidc.config: |
name: Keycloak
issuer: https://sso.example.com/realms/platform
clientID: argocd
clientSecret: $oidc.keycloak.clientSecret
requestedScopes: ["openid", "profile", "email", "groups"]
issuer는 browser에서 보이는 login URL이 아니라 realm의 정확한 OIDC issuer여야 한다. Argo CD가 issuer의 discovery와 signing key endpoint에 도달할 수 있는지도 network·CA trust와 함께 확인한다.
self-signed certificate 검증을 끄는 설정으로 문제를 덮지 않는다. internal CA를 Argo CD가 신뢰하도록 배포하고 hostname과 chain을 올바르게 맞춘다.
group claim과 RBAC를 연결한다
Keycloak이 /platform-readers, /platform-deployers group claim을 보낸다고 가정하면 RBAC는 다음처럼 분리할 수 있다.
p, role:platform-readonly, projects, get, sample-apps, allow
p, role:platform-readonly, applications, get, sample-apps/*, allow
p, role:platform-syncer, projects, get, sample-apps, allow
p, role:platform-syncer, applications, get, sample-apps/*, allow
p, role:platform-syncer, applications, sync, sample-apps/*, allow
g, /platform-readers, role:platform-readonly
g, /platform-deployers, role:platform-syncer
group path에 leading slash가 붙는지, nested group이 전체 path로 나오는지, claim이 string인지 array인지는 Keycloak mapper 설정에 따라 달라질 수 있다. 다른 환경의 예제 문자열을 복사하지 않고 자신의 token과 Argo CD login claim을 확인한다.
JWT payload는 Base64URL encoding일 뿐 encryption이 아니다. credential과 개인정보가 든 token을 online decoder에 붙여 넣지 않는다. local에서 header·payload를 확인하더라도 trust 판단에는 issuer, audience, expiry, signature와 key rotation 검증이 필요하다.
OAuth 2.0·OIDC·JWT를 섞어 부르지 않는다
- OAuth 2.0은 delegated authorization framework다.
- OpenID Connect는 OAuth 2.0 위에 identity layer를 추가한다.
- ID token은 client가 authentication 결과와 user identity claim을 확인하는 데 쓴다.
- Access token은 protected API 접근 권한을 나타낸다.
- JWT는 token을 표현할 수 있는 형식 중 하나다. JWT라는 이유만으로 암호화되거나 안전한 것은 아니다.
Argo CD SSO에서는 OIDC authentication 결과의 subject·group claim을 RBAC subject에 연결한다. access token·ID token의 역할을 바꾸거나 payload만 decode하고 서명 검증을 생략해서는 안 된다.
운영 전 access matrix로 검증한다
설정 file만 읽고 완료로 보지 않는다. test identity와 격리된 application으로 positive·negative case를 모두 확인한다.
| 주체 | sample-apps 조회 | sample-apps sync | 다른 project 조회 | account/token 관리 |
|---|---|---|---|---|
| group 없는 login user | 거부 | 거부 | 거부 | 거부 |
| platform-readers | 허용 | 거부 | 거부 | 거부 |
| platform-deployers | 허용 | 허용 | 거부 | 거부 |
| ci-deployer project token | 허용 | 허용 | 거부 | 거부 |
| break-glass operator | 승인된 범위 | 승인된 범위 | policy에 따름 | 별도 승인 |
추가로 확인한다.
- 만료된 project token이 실패하는가?
- revoke 뒤 기존 token이 더는 동작하지 않는가?
- Keycloak group 제거가 Argo CD 권한에서 사라지는가?
- SSO session과 refresh token lifetime이 조직 정책과 맞는가?
- login·sync·RBAC 변경 audit log가 중앙에 남는가?
- Keycloak 장애 때 break-glass 절차가 실제로 가능한가?
- Argo CD upgrade 뒤 RBAC fine-grained behavior가 바뀌지 않았는가?
접근 제어의 목표는 모든 사용자를 readonly로 만드는 것이 아니다. 인증된 주체마다 필요한 project와 action만 허용하고, default 권한과 credential 수명을 작게 유지하는 것이다.
다음 글인 Argo CD로 Kubernetes cluster bootstrap에서는 이 권한 model을 cluster onboarding에 연결할 수 있다. Jenkins·Keycloak·OpenLDAP까지 묶은 실습은 Kubernetes SSO와 RBAC 부록에서 이어진다.
참고 자료
'배움과 성장 > DevOps·클라우드' 카테고리의 다른 글
| kind 멀티 클러스터 Argo CD 부트스트랩: App of Apps와 ApplicationSet 실습 기록 (0) | 2025.11.22 |
|---|---|
| 『웹 엔지니어가 알아야 할 인프라의 기본』 1장: RAS·CIA를 설계 질문으로 바꾸기 (1) | 2025.11.19 |
| SLO·에러 버짓·RTO·MTTR: 신뢰성 지표를 섞지 않는 법 (1) | 2025.11.09 |
| Argo CD 운영 체크리스트: HA·Sync Waves·백업·모니터링 (1) | 2025.11.08 |
| Argo CD 설치 후 첫 Application: 수동·자동 동기화와 보안 경계 (0) | 2025.11.08 |
댓글