<!-- canonical: https://docs.axelabs.ai/ops/runbook/secret-rotation -->
<!-- source: content/ops/runbook/secret-rotation.mdx -->

---
title: Secret Rotation
description: 모든 비밀 회전의 단일 명령 — axe secret rotate. Vault SoT + external provider (Azure/Meta/Naver/...) 동기화 + 무중단 재배포 자동화.
playbook: true
---

# Secret Rotation

## AI 요청 프롬프트

```
https://docs.axelabs.ai/ops/runbook/secret-rotation 따라 [secret name] 회전 진행해줘.

진행:
1. 회전 대상 secret + provider 식별 (Azure / Meta / Naver / GitHub / Anthropic / 기타)
2. Azure 면 az cli 6 단계 분기, 그 외면 `axe secret rotate <ENV_NAME>` interactive 분기
3. 페이지의 각 step 명령 실행 + 검증, 매 step 결과 받고 다음. 특히 OLD secret revoke 직전 사용자 확인 (NEW secret 무중단 swap + production 검증 통과 후)
4. 함정 발생 시 페이지 본문 따라 우회 (App Owner 부재 / external portal 수동 / `unset NEW` shell history 정리 / merge-mode pull 누락)
5. 회전 완료 + production health 검증 + (선택) /ops/updates Ship Log 한 줄
```

본인 AI session = Claude Code / Cursor / ChatGPT 데스크탑 / Claude.app / 기타.

페이지 본문 = 사람이 직접 read 도 가능, AI 도 참고. AI 가 본 페이지 fetch 후 위 진행 순서대로 사용자와 step-by-step interactive 풀어나감.

> **D-ops-17 + D-ops-19 (2026-05-21)** 이후 표준: Azure 의 client_secret 은 **az cli 6 단계** 로 portal UI 0회. 다른 provider (Meta/Naver/GitHub/Anthropic) 는 `axe secret rotate` 의 interactive 경로 + 외부 portal 수동.

## Azure 회전 — az cli 6 단계 (권장)

```bash
APP_ID=137fc0ef-eb9f-4903-acbc-1a748add349c   # frame_mcp (예시)
OLD_KEY_ID=$(az ad app credential list --id $APP_ID \
  --query "[?starts_with(hint,'<old prefix>')].keyId | [0]" -o tsv)

# 1. Azure 새 secret 추가 — --append 가 OLD 유지 = overlap window
NEW=$(az ad app credential reset --id $APP_ID \
  --display-name "auto-rotated 2026-MM-DD" --years 2 --append \
  --query password -o tsv)

# 2. vault PUT
axe secret push AZURE_FRAME_MCP_CLIENT_SECRET --service frame --value "$NEW"

# 3. pull (merge-mode — config 보존)
axe secret pull frame

# 4. 무중단 swap
axe deploy frame axe --apply          # frame / hive
axe blueprint upgrade axe --apply     # blueprint

# 5. 검증
docker exec $(active container) env | grep AZURE_FRAME_MCP_CLIENT_SECRET
/usr/bin/curl -sI https://axe.axelabs.ai/frame/health   # 200

# 6. OLD revoke (검증 후)
az ad app credential delete --id $APP_ID --key-id $OLD_KEY_ID

unset NEW                              # shell history 에서 제거
```

### 전제조건 — App Owner 확인

`az ad app credential reset` 은 app 의 owner 만 가능. portal-등록 app 은 default 로 owner 없음:

```bash
az ad app owner list --id $APP_ID --query "[].userPrincipalName" -o tsv
# 비어 있으면 → portal 에서 owner 추가 후 재시도:
#   open "https://portal.azure.com/#view/Microsoft_AAD_RegisteredApps/ApplicationMenuBlade/~/Owners/appId/$APP_ID"
```

본 플랫폼의 모든 AXE app 은 `ai@axellc.com` owner 박혀 있음 (2026-05-21 audit). 신규 az cli 등록 app 은 호출자가 owner 자동 박힘.

## 다른 provider 회전 — `axe secret rotate` interactive

Anthropic / GitHub / Meta / Naver / Slack 등 az 안 통하는 provider:

```bash
axe secret rotate <ENV_NAME> --service <svc>
```

진행:
1. 매니페스트의 `rotation_external` 확인 → provider portal URL 출력
2. 운영자가 portal 에서 새 값 발급 → 복사 → 프롬프트에 붙여넣기 (`getpass`, 화면 안 보임)
3. vault PUT
4. `axe secret pull SVC` (merge-mode)
5. `axe ship SVC` 트리거 (working tree dirty 면 abort — `axe deploy ... --apply` 로 수동 보완)
6. 운영자가 provider 에서 OLD 수동 revoke

상세 데이터 흐름: [/architecture/secrets](/architecture/secrets).

## 회전 주기 — 정기 점검

Microsoft Entra ID app 의 client_secret 회전 주기 = 24개월 (Frame MCP), 180일 (Blueprint, Vaultwarden).

만료 30일 전 자동 알림. 미루지 말 것 — 만료 = OAuth 전체 마비.

## 사전 알림

**현재 상태**: `com.axe.secret-check` launchd 자동 알림 + `axe secret status --customer X` CLI 는 **향후 추가 예정** (TODO). 현재는 수동 확인:

```bash
# 운영자 콘솔 (admin.axelabs.ai) 에 secret 만료일 표시 — `axe console rebuild` 가 매시 갱신
# 또는 Vault 에 보관된 메타에서 직접 확인:
bw get item "Frame MCP — claude.ai connector secret"  # 만료일 필드 확인
```

출력 예:
```
axe customer:
  blueprint:    expires 2026-08-15 (89 days)
  vaultwarden:  expires 2026-09-01 (106 days)
  frame_mcp:    expires 2028-05-19 (730 days)
```

90일 이내면 운영자 콘솔 dashboard 에 빨간 배너.

## 회전 절차 (180일 secret, ~30분)

예: customer `axe` 의 `frame_mcp` secret 회전 (24개월 만료).

### 1. 새 secret 발급 (customer IT 측)

customer IT 에게 메일:

```
제목: [AXE Labs] Frame MCP client_secret 갱신 요청 (만료 30일 전)

안녕하세요,

귀사 Microsoft Entra ID 의 'Frame MCP' app 의 client_secret 이 
30일 후 만료됩니다 (2028-05-19).

다음 절차로 새 secret 발급 + 안전 채널로 전달 부탁드립니다:

1. Azure portal → Microsoft Entra ID → App registrations → 'Frame MCP'
2. Certificates & secrets → + New client secret
3. Description: frame-mcp-axe-2028-05
4. Expires: 24 months
5. Add → VALUE 즉시 복사
6. Bitwarden Send (view-once, `-a 1 -d 1`) 또는 안전 채널로 운영자에게 전달 — 절차: [/architecture/secrets § 사람에게 전달](/architecture/secrets#사람에게-전달--bitwarden-send)

운영자 측 swap 완료 후 기존 secret 은 안전하게 폐기됩니다.

감사합니다.
액스코퍼레이션 주식회사 (운영 주체, ai@axellc.com)
```

### 2. 새 secret 받으면

```bash
# Keychain push (replace 기존)
security add-generic-password -a axe -s "axe.axe.frame.client_secret" -w '&lt;new_value&gt;' -U

# Verify
security find-generic-password -s "axe.axe.frame.client_secret" -w
# → &lt;new_value&gt; 출력되면 OK
```

### 3. 컨테이너 재기동 (env reload)

```bash
cd /Users/axe/frame
set -a && source .env.local && set +a
docker compose up -d --force-recreate frame-mcp-blue frame-mcp-green
sleep 5

# Verify env 적용
docker exec frame-mcp-blue env | grep AZURE_FRAME_MCP_CLIENT_SECRET | head -c 30
# → 새 값의 prefix 보이면 OK
```

> ⚠️ **다운타임 ~3초** — `--force-recreate` 가 blue+green 동시 재기동. 향후 blue→green→blue 순차 재기동으로 0초 만들 예정 (D-ops-16 후보).

### 4. claude.ai connector 측 영향

`frame_mcp` secret 의 경우 **직원 측 영향 있음**:
- claude.ai 의 Custom Connector 에 입력한 secret 도 갱신 필요
- 운영자 → 직원들에게 새 secret 전달 (안전 채널)
- 직원 각자 claude.ai 의 connector 편집 → Advanced 의 OAuth Client Secret 교체 → Save

> ⚠️ 이게 secret 회전의 가장 큰 운영 부담. 직원이 많을수록 painful. 향후 OAuth proxy 패턴 (D-ops-15) 으로 secret 분배 제거 검토.

### 5. 기존 secret 폐기 (24시간 후)

새 secret 활성 확인 + 직원 전원 swap 완료 후:

```
# customer IT 에게:
"기존 client_secret (<처음 4글자만 hint, 예: XX.X~ 처럼 식별 가능한 prefix>)
 Azure portal 에서 삭제 부탁드립니다.
 새 secret 으로 swap 완료 + 24시간 검증 완료했습니다."
```

> ⚠️ **secret VALUE 평문을 docs/메일/채팅 어디에도 적지 마세요**. 식별 hint 는 Azure portal 의 secret 표에서 보이는 hint prefix (보통 처음 3-4자 + "...") 만 사용. 운영자 본인이 회전 시점에는 Keychain `find-generic-password` 로 prefix 확인.

customer IT 측에서 Azure portal → Certificates & secrets → 기존 secret 옆 휴지통 클릭.

### 6. 검증

```bash
# customers.yaml 메타 업데이트 (선택)
# sso.apps.frame_mcp.client_secret_env 의 만료일 주석 갱신

# Audit (TODO: `axe secret status` 추가 후)
# 현재는 Vault item 의 만료 메타 갱신 + 운영자 콘솔 dashboard 확인
```

## 함정

| 함정 | 결과 | 회피 |
|---|---|---|
| 만료일 잊고 24시간 후 마비 | OAuth 전체 다운 | 매일 자동 알림 + 30일 전 작업 |
| 새 secret swap 전 기존 삭제 | OAuth 즉시 다운 | 24시간 grace period |
| Keychain replace 안 하고 새로 add | duplicate, 어떤 게 active 인지 모름 | `-U` 플래그로 update |
| 컨테이너 restart 만 (--force-recreate X) | env file 재로드 안 됨 | `up -d --force-recreate` |
| 직원에게 secret 일괄 push 잊음 | 직원들 connector 실패 | swap 전에 communication |

## 응급: secret 노출 의심

평문이 어딘가 노출된 의심이 있을 경우 (메일 전송 사고, 노트북 분실 등):

1. customer IT 에게 즉시 새 secret 발급 요청 (정상 절차 X, 비상)
2. 받자마자 Keychain push + 컨테이너 재기동
3. 기존 secret 즉시 삭제 (24시간 grace X)
4. audit_log 검사 — 의심 시간대에 비정상 access 있는지
5. 직원 전원에게 알림 + claude.ai connector 즉시 update 요청

상세: [Runbook · Secret 노출 대응](/ops/runbook/secret-incident) (예정).
