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

---
title: 신규 Customer Onboarding
description: 운영자 측 자동화 + 수동 touchpoints. D-day = vault 비번 + axe 2~3 줄.
playbook: true
---

# 신규 Customer Onboarding

## AI 요청 프롬프트

```
https://docs.axelabs.ai/ops/runbook/customer-onboarding 따라 신규 customer ([customer id]) D-day onboard 진행해줘.

진행:
1. D-day prerequisite 확인 — vault session unlock (bw + Keychain `axe.vault.session`, 8h cache) + customer IT 회신 `axelabs-bootstrap-{customer}.json` pack 수령 + customer macmini Tailscale ACTIVE direct
2. 페이지 Phase B 표 따라 5 step 중 현재 자동화 상태 분기 — `axe customers ingest` → `axe onboard --apply` → `axe deploy blueprint --apply` → `axe deploy hive --apply` (umbrella 미구현 시 순차)
3. 각 axe 명령은 dry-run (`--apply` 생략) 먼저 실행 + 변경 계획 사용자 확인 후 `--apply`. 매 step 결과 받고 다음. SSH 진입 / cloudflared ingress / vault collection 권한 점검 포함
4. 함정 발생 시 [/ops/known-gaps](/ops/known-gaps) 의 "D-day traps" 섹션 표 따라 우회 (Tailscale short alias / SSH non-login PATH / TXT+CNAME 공존 / keychain partition / bw cache stale / vault DOMAIN path / docker volume external 부재 / customers.yaml directory 잘못 생성 등 15+ 함정)
5. 5 step 완료 후 customer 측 health 검증 (frame/blueprint/hive 외부 endpoint 200) + Ship Log 한 줄 + customer IT 회신 ("배포 완료, 접속 안내")
```

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

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

신규 customer (예: realchoice) 의 macmini 에 AXE Labs 스택 전체를 배포하는 절차.

## D-day TLDR — vault 먼저 풀어두면 1 명령

> **목표** ([B-onboard-1shot](/ops/backlog)): D-day 운영자 입력 = **axe 명령 1 줄**.
> vault 의 `bw unlock` 은 **session prerequisite** (8 시간 cache) — D-day touchpoint 아님.

### Phase A — 세션 prerequisite (한 번, 이후 8 시간 자동)

| 작업 | 명령 |
|---|---|
| vault session unlock + Keychain cache | `bw unlock --raw \| security add-generic-password -s axe.vault.session -a ai@axellc.com -w "$(cat)" -U` |

이후 모든 `axe secret`/`axe onboard`/`axe deploy` 명령은 Keychain 의 BW_SESSION 자동 사용. 비번 재입력 없음.

### Phase B — Deploy touchpoint (umbrella 후 1 명령, 현재 5 명령)

| # | 운영자가 손대는 부분 | 자동화 | backlog |
|---|---|---|---|
| 0 | customer IT 에게 docs link 4 개 송부 (D-7) | ✅ 자력 — bootstrap.sh 가 docs.axelabs.ai 의 raw 로 노출 (2026-05-23 해소) | [B-onboard-bootstrap-publish](/ops/backlog) ✅ |
| 1 | `customers.yaml` 의 `{customer}` 블록 + services 슬롯 작성 | `axe customers add` = stub | [B-onboard-customers-add](/ops/backlog) |
| 2 | customer IT pack.json → vault + customers.yaml 흡수 | ✅ `axe customers ingest {id} {pack}` (2026-05-23 구현) — dry-run/`--apply` + ruamel round-trip (comment 보존) + idempotent | [B-onboard-azure-pack](/ops/backlog) ✅ |
| 3 | `axe onboard {customer} --apply` (cloudflared + frame, 18-step) | ✅ 자동 | — |
| 4 | `axe deploy blueprint {customer} --apply` (Blueprint, 11-step) | ✅ 자동 (SSO 미설정 부팅) | [B-onboard-bp-sso](/ops/backlog) |
| 5 | Hive stack 수동 (ssh + docker compose) | `axe deploy hive` 부재 | [B-onboard-hive-deploy](/ops/backlog) |

→ 위 5 개를 묶는 **umbrella 명령** [B-onboard-umbrella](/ops/backlog) 가 구현되면:

```bash
# 세션 prerequisite (8 시간 1 회)
bw unlock --raw | security add-generic-password -s axe.vault.session -a ai@axellc.com -w "$(cat)" -U

# D-day — 줄 1 개
axe deploy customer {customer} --from-pack ~/Downloads/axelabs-bootstrap-{customer}.json --apply
```

운영자 D-day 입력 = **0 회** (vault 살아있으면) 또는 **1 회** (vault 만료 시 unlock 한 번).

> 진정한 0 입력 D-day = (a) cron 이 `bw unlock` keep-alive 유지 + (b) umbrella 구현 + (c) pack.json 사전 수신. (c) 는 customer IT 측이 axelabs-bootstrap.sh 실행해서 안전채널로 보내주는 시점 의존 — 즉 운영자는 IT 회신만 기다리면 됨.

## D-day 실제 명령 (2026-05-26 시점, B-customer-deploy-generalization Phase 1 ✅)

```bash
# 0. 운영자 vault unlock (master password 1 회)
export BW_SESSION="$(bw unlock --raw)"
security add-generic-password -s axe.vault.session -a ai@axellc.com -w "$BW_SESSION" -U

# 1. customer IT 회신 받은 axelabs-bootstrap-{customer}.json 흡수 (one-shot)
#    sso.tenant_id + sso.apps.{}.client_id fill + services manifest 슬롯 추가
#    + vault push × 3 (3 client_secret) 묶음
axe customers ingest {customer} ~/Downloads/axelabs-bootstrap-{customer}.json --apply
#    (dry-run 으로 먼저 확인하려면 --apply 빼고 실행 → 변경 계획만 출력)

# 2. Cloudflare tunnel + DNS + cloudflared launchd 부트스트랩 (1-9 step)
axe onboard {customer} --apply --skip-frame
#    --skip-frame: frame stack 은 별도 `axe deploy frame` 로. 이전 18-step 묶음 패턴 폐기.

# 3. customer 측 bw vault session bootstrap (interactive 1회 — customer IT 가 실행)
#    operator 가 SSH 으로 helper 를 push 한 후 customer IT 에게 실행 요청:
scp /Users/axe/.axe/bw-bootstrap.sh {customer}-macmini:~/bw-bootstrap.sh
ssh -t {customer}-macmini "brew install bitwarden-cli && \
    ~/bw-bootstrap.sh https://<vault-url> <login-email>"
#    → ~/.bw-session (mode 600) 생성. wrapper 가 SSH non-interactive 호출 가능해짐.

# 4. service 3종 customer-측 deploy (각각 13-step, ~5-15 분 each)
axe deploy frame {customer} --apply
axe deploy blueprint {customer} --apply
axe deploy hive {customer} --apply
```

> ⚠️ **위 `{customer}` 는 `services:` 매니페스트를 선언한 *신규* customer 자리** — sovereignty/self-deploy 로 졸업한 고객(realchoice/Truvia)은 **대상 아님**. 그 고객은 자기 macmini 에서 secret·compose·Caddy 를 **자체 배포**하고, `customers.yaml` 에 `services:` 매니페스트가 *의도적으로* 없어 `axe deploy {svc} {customer}` 가 `services … not declared` 로 `sys.exit` 한다. 운영자 역할 = **software supply (code/image) + 외부 노출 (DNS·cloudflared catch-all)** 뿐. 근거: [/ops/decisions](/ops/decisions) (D-ops-40 / B-customer-sovereignty-architecture).

각 `axe deploy {service} {customer}` 가 동일한 13-step 흐름 수행:
preflight → clone → vault_check → secrets_bootstrap (auto-gen if missing) → env_local → wrapper push → network → (blueprint 만) frame_mcp_token → compose up → (frame 만) proxy push → health → (frame+hive) register_entities → ingress swap.

**`axe onboard --skip-frame` 권장 이유**: 이전 18-step 통합 흐름은 frame stack 까지 묶였으나, 이제 customer-측 deploy 가 일반화되어 `axe deploy frame {customer}` 로 분리 호출이 idempotent + 재시도 안전. onboard 는 cloudflared layer 만 담당.

## D-14 ~ D-7 (사전 협의)

| 작업 | 책임 |
|---|---|
| 1. 가격·SLA 협의 | 운영자 |
| 2. customer ID 정함 (`x`, `newco` 등) | 운영자 |
| 3. customer 측 IT 담당자 식별 | customer |
| 4. macmini 구매 (M2/M3, 16GB+, 512GB+) | customer |
| 5. Tailscale 가입 + key 교환 | 양측 |
| 6. customer 측 Microsoft 365 tenant 확인 | customer |
| 7. customer 측 도메인 검증 준비 (TXT record 추가 권한) | customer IT |
| 8. axelabs.ai zone 의 `{customer}` A record 추가 | 운영자 (Cloudflare) |

## D-7 (Azure 사전 등록 — customer IT 자력)

운영자가 customer IT 에게 보내는 것 = **링크 4 개** (별도 첨부 / 스크립트 메시지 X — 2026-05-23 `B-onboard-bootstrap-publish` 해소):

```
https://docs.axelabs.ai/partner             ← 4 단계 흐름 (entry)
https://docs.axelabs.ai/partner/macmini-prep  ← macmini 사전 준비 (Tailscale, SSH, 절전)
https://docs.axelabs.ai/partner/registration  ← Option A CLI 1 줄 (axelabs-bootstrap.sh)
https://docs.axelabs.ai/partner/handoff       ← JSON pack 회신 양식
```

customer IT 가 30 분 (Option A) ~ 45 분 (Option B) 작업 → `axelabs-bootstrap-{customer}.json` 회신 (또는 텍스트 8 개 값).

JSON pack 스키마: `axelabs-bootstrap/v1` — `tenant_id` + `apps.blueprint/vaultwarden/frame_mcp` 각각의 `client_id` + `client_secret` + (frame_mcp 만) `application_id_uri` + `scope` + `redirect_uris`. 운영자가 받으면 [B-onboard-azure-pack](/ops/backlog) 의 `axe customer ingest` 가 1 줄로 흡수 (구현 후) — 현재는 수동 paste + `axe secret push` × 3.

운영자가 받으면:
1. **client_secret 3개 → vault 로 push** (Keychain 직접 push 는 폐기 — 비밀의 SoT = Vaultwarden, [D-ops-17](/ops/decisions#d-ops-17--secret-deploy-time-pull)):
   ```bash
   axe secret push AZURE_{CUSTOMER}_BLUEPRINT_CLIENT_SECRET   --service blueprint   --customer {customer}
   axe secret push AZURE_{CUSTOMER}_VAULTWARDEN_CLIENT_SECRET --service vaultwarden --customer {customer}
   axe secret push AZURE_{CUSTOMER}_FRAME_MCP_CLIENT_SECRET   --service frame       --customer {customer}
   ```
   (각 명령은 `Value for ...:` 프롬프트 → 안전채널로 받은 값 붙여넣기. 매니페스트에 customer service 슬롯이 사전 등재돼 있어야 함 — [B-onboard-azure-pack](/ops/backlog).)
2. `customers.yaml` 에 entry 추가:
   ```yaml
   {customer}:
     legal_name: "<...>"
     primary_domain: "<...>"
     public_domain: "{customer}.axelabs.ai"
     entities: ["<...>"]
     tailscale_host: "{customer}-macmini"
     sso:
       provider: "microsoft_entra_id"
       tenant_id: "<...>"
       apps:
         blueprint:
           client_id: "<...>"
           client_secret_env: "AZURE_{CUSTOMER}_BLUEPRINT_CLIENT_SECRET"
           redirect_uri: "https://{customer}.axelabs.ai/api/auth/callback/azure-ad"
         vaultwarden:
           client_id: "<...>"
           client_secret_env: "AZURE_{CUSTOMER}_VAULTWARDEN_CLIENT_SECRET"
           redirect_uri: "https://{customer}.axelabs.ai/vault/identity/connect/oidc-signin"
         frame_mcp:
           client_id: "<...>"
           client_secret_env: "AZURE_{CUSTOMER}_FRAME_MCP_CLIENT_SECRET"
           application_id_uri: "https://{customer}.axelabs.ai/frame/mcp"
           redirect_uris:
             - "https://claude.ai/api/mcp/auth_callback"
             - "https://claude.com/api/mcp/auth_callback"
           scopes:
             - "openid"
             - "profile"
             - "email"
             - "https://{customer}.axelabs.ai/frame/mcp/mcp.access"
     user_entity_map: {}
     default_entities_by_domain:
       "&lt;primary_domain&gt;": ["&lt;entity&gt;"]
     onboarded: "&lt;date&gt;"
   ```
3. ~~cloudflared 중앙 tunnel ingress 편집~~ — **불필요** ([2026-05-23 drift 정정](/ops/known-gaps)). `axe onboard` 가 **customer 별 독립 tunnel** 을 customer macmini 에 생성/launchd 등록함 (`_render_cloudflared_config` in `/Users/axe/.axe/bin/axe`). 중앙 `/Users/axe/.axe/tunnels/axelabs/config.yml` 편집 의 D-7 의무는 사라짐. axe macmini 측 cloudflared 는 docs.axelabs.ai / admin.axelabs.ai 등 운영자 자기 서비스 전용.
4. ~~cloudflared 재시작~~ — 위와 동일. customer 측 tunnel 은 `axe onboard` step 8 의 launchd 가 자동 boot.
5. Cloudflare zone 의 axelabs.ai 에서 `{customer}` A record 추가 — **불필요**. `axe onboard` step 5 가 Cloudflare API 로 자동 CNAME 생성 (`<tunnel-uuid>.cfargotunnel.com`).

## D-day (~1 시간, 자동)

운영자 머신에서 — 상단 [D-day TLDR](#d-day-tldr--운영자-수동-touchpoints) 참조. 핵심 흐름:

```bash
# Dry-run 으로 확인 (변경 0)
axe onboard {customer}

# 실제 적용 (cloudflared + frame)
axe onboard {customer} --apply

# Blueprint (별도 명령 — 통합은 B-onboard-1shot)
axe deploy blueprint {customer} --apply
```

`axe onboard` 가 18-step 을 순차 실행 (Tailscale SSH 로 customer macmini 에 push):

| Step | 작업 (실제 axe CLI 코드 기준) |
|---|---|
| 1 | Tailscale up / customer macmini reachable 확인 |
| 2 | customer meta 검증 (customers.yaml entry, Tailscale FQDN) |
| 3 | SSH probe (key 인증) |
| 4 | cloudflared 터널 생성 (기존 있으면 reuse) |
| 5 | Cloudflare DNS A record 생성 |
| 6 | cloudflared credentials.json push → macmini |
| 7 | cloudflared config.yml push (ingress 규칙 포함) |
| 8 | cloudflared launchd plist 등록 + boot |
| 9 | cloudflared health check (curl /cdn-cgi/trace) |
| 10 | frame git clone + Dockerfile preflight |
| 11 | frame git submodule + asset 동기화 |
| 12 | macmini Keychain 에 secret push (vault, frame, blueprint, restic) |
| 13 | macmini .env.local 생성 (env_file 용) |
| 14 | docker compose up (postgres + frame-mcp-blue + frame-mcp-green + proxy) |
| 15 | frame-mcp /health/ready poll (max 60s) |
| 16 | `frame register-entity` × 각 entity + `frame migrate` |
| 17 | axe-frame-proxy alias = blue 로 설정 |
| 18 | cloudflared ingress alias swap (axe-frame-proxy 가 frame upstream) |

진행 중 어느 step 에서 실패하면 자동 rollback (기존 상태 유지). 운영자 화면에 명확한 오류 + 다음 단계 제시.

## D+1 (검증 + Vault 부트스트랩)

운영자 (`ai@axellc.com`) 측에서:

```bash
# 외부 health
axe health {customer}

# customer macmini ssh
ssh {customer}-macmini "docker ps"

# Blueprint admin 부여
ssh {customer}-macmini "docker exec blueprint-app pnpm prisma db execute --stdin" <<EOF
UPDATE User SET role = 'admin' WHERE email = '&lt;ADMIN_EMAIL&gt;';
EOF

# 첫 frame entity 등록 (chart auto-seed 적용, D-ops-21)
ssh {customer}-macmini "docker exec frame-mcp-blue python -m frame.cli register-entity --id &lt;entity_id&gt; --legal-name '<법인명>' --kind corporate --accounting-standard ksme"

# 첫 hive entity 등록 (frame mirror)
ssh {customer}-macmini "docker exec hive-mcp-blue python -m hive.cli register-entity --id &lt;entity_id&gt; --legal-name '<법인명>'"

# PII passphrase 2개 vault push + frame/hive deploy (axe customer 의 deploy-axep.sh 패턴)
# 일반화된 deploy-new-entity.sh 가 자동 처리 (D-ops-31 후속 일반화):
#   bash /Users/axe/.axe/vault/deploy-new-entity.sh &lt;customer&gt; &lt;entity_id&gt; "&lt;법인명&gt;"
```

### Vault 부트스트랩 — `axe vault bootstrap {customer}` ([D-ops-33](/ops/decisions))

운영자 mac 에서 1 명령:

```bash
# Dry-run (default) — 현재 4 key 적용 상태 진단
axe vault bootstrap {customer}

# 실 패치 + axe-vaultwarden force-recreate
axe vault bootstrap {customer} --apply
```

명령 자동 흐름:
1. SSH 로 customer macmini 의 `/Users/{ssh_user}/.axe/vault/docker-compose.yml` 읽음 (axe customer 는 local exec)
2. 4 key (D-ops-24/26/27) 적용 상태 진단 — 이미 OK 면 skip
3. 누락 key 만 patch (in-place, anchor = `SSO_SIGNUPS_MATCH_EMAIL` 다음 줄)
4. `docker compose up -d --force-recreate axe-vaultwarden`
5. Idempotent — 재실행 안전

적용되는 4 key:
```yaml
SSO_ALLOW_UNKNOWN_EMAIL_VERIFICATION: "true"   # D-ops-24
SIGNUPS_ALLOWED:                       "true"  # D-ops-26
SSO_ONLY:                              "false" # D-ops-27 — emergency MP fallback 보존
ORGANIZATION_INVITE_AUTO_ACCEPT:       "true"  # D-ops-27 — 직원 Accept 자동
```

(`INVITATIONS_ALLOWED=false` 는 기존 docker-compose 의 default — JIT 통일.)

이 명령 빠지면 첫 employee 가 "Failed to retrieve the invitation" / "Your provider does not send email verification status" 등 차단 ([D-ops-24](/ops/decisions), [D-ops-26](/ops/decisions), [D-ops-27](/ops/decisions) 참조).

## D+2 (직원 onboarding + Vault 첫 멤버 invite)

### 직원 SSO 안내

운영자 → customer admin 으로 [신규 직원 온보딩 URL](/onboard) 전달.

customer admin 이 자기 직원들에게 안내. 각자 [Frame connector 4-step setup](/onboard/claude-frame-setup) 따라 설정.

Vault 접속 안내 ([troubleshooting](/onboard/troubleshooting) 의 Vault 섹션 참조):
- 직링크: `https://{customer}.axelabs.ai/vault/#/sso`
- Identifier: 그 customer 의 org 이름 (예: AXE, REALCHOICE)

### Vault Organization + 직원 invite

customer admin (또는 운영자 SSH proxy) 가:

1. Web vault → Admin Console → org 생성 (`{CUSTOMER}` 대문자 권장, 예: AXE / REALCHOICE)
2. **dual-identity 권장 패턴** ([D-ops-29](/ops/decisions)):
   - `ai@{customer-domain}` = 자동화 / bot identity (Graph token 발행, proactive DM, agent run)
   - 실제 admin 직원 별도 계정 = 사람 작업 identity
   - 둘 다 vault Owner + access_all=1
3. 직원 invite — `/Users/axe/.axe/vault/invite-members.sh` 패턴 (customer 별 변수만 교체)
4. **Collection 구조 v1** ([D-ops-32](/ops/decisions)):
   - entity 1개 customer (예: realchoice): 4-collection 단순 구조
     - `Platform — Service Secrets` (operator only)
     - `Platform — Infrastructure` (operator only)
     - `{Customer Entity}` (전직원 RW)
     - `Shared Tools` (전직원 RW)
   - entity 다수 customer (예: axe 의 axec/axev/axep): 6-collection (entity별 분리)

## D+7 (안정화 점검)

- frame `audit_log` 활성? (실제 분개 1건 이상 발생?)
- backup 정상? (`restic -r /Users/axe/.axe/backups/local snapshots --password-file ...`)
- 직원 첫 시도 시 누락 없음?

## 함정

| 함정 | 결과 | 회피 |
|---|---|---|
| customers.yaml entry 빠뜨림 | onboard 18-step 실패 | step 2 가 자동 검증 |
| ~~Cloudflare A record 없음~~ | (해소 — `axe onboard` step 5 가 자동 생성) | — |
| customer macmini sleep mode | onboard SSH 실패 | D-7 까지 절전 안 함 설정 |
| Tailscale key 교환 안 됨 | step 1 실패 | D-7 까지 양방향 ping 검증 |
| **customers.yaml `services:` 섹션에 customer 슬롯 없음** | `axe secret push` 가 매니페스트 lookup 실패 (`{env_name} not in manifest`) | `services.{svc}.{customer}.secrets[]` 슬롯 사전 등재. 현재 `axe` 만 등재됨 — realchoice 는 신규 작업 ([B-onboard-azure-pack](/ops/backlog)) |
| **Cloudflare API token vault 미등재** | `axe onboard` step 4 의 `_cf_token()` 실패 | 첫 onboard 전 1 회 `axe secret push` 로 등재 ([B-onboard-cf-token-doc](/ops/backlog)) |
| **Blueprint 가 SSO 미설정으로 부팅** | `axe deploy blueprint` 후 첫 로그인 실패 | 현재 `cmd_deploy_blueprint` docstring 명시 한계. customer Keychain 에 Azure secret 별도 push 필요 ([B-onboard-bp-sso](/ops/backlog)) |
| **Hive stack 자동 배포 명령 부재** | `axe deploy hive` 가 없어서 매번 ssh + docker compose 수동 | [B-onboard-hive-deploy](/ops/backlog) |
| Vault `SSO_ALLOW_UNKNOWN_EMAIL_VERIFICATION` 미설정 | 첫 employee SSO "Your provider does not send email verification status" 차단 | D+1 의 Vault 부트스트랩 4-block ([D-ops-24](/ops/decisions)) |
| Vault `SIGNUPS_ALLOWED=false` + `INVITATIONS_ALLOWED=false` | 첫 employee JIT 가입 "Failed to retrieve the invitation" 차단 | D+1 의 4-block ([D-ops-26](/ops/decisions)) |
| Vault `SSO_ONLY=true` 설정 | Entra 장애 시 운영자 lockout (master password 진입 불가) | `SSO_ONLY: "false"` 유지 ([D-ops-27](/ops/decisions)) |
| Vault UI 이메일-first 화면이 SSO Identifier 가림 | 직원이 가입 절차 못 찾음 | `/vault/#/sso` 직링크 안내 ([troubleshooting](/onboard/troubleshooting)) |
| frame/hive entity 추가 시 PII passphrase 빠뜨림 | 그 entity 의 PII 작업 시점에 RuntimeError | `deploy-new-entity.sh` 가 PII passphrase 2개 vault push + ship 자동화 ([D-ops-31](/ops/decisions)) |
| customer Vaultwarden 의 bw CLI 자동화 시 BW_SESSION 직접 export 만 함 | `axe secret push` 실패 (keychain 만 읽음) | `security add-generic-password -s axe.vault.session -a ai@... -w "$BW_SESSION" -U` 단계 통합 ([D-ops-31](/ops/decisions) 부수 발견) |
| Vaultwarden Timshel 1.34.1-6 + bw CLI 2026.x | `TypeError: ... toWrappedAccountCryptographicState` crash | bw CLI 2025.7.0 pin (`npm install -g @bitwarden/cli@2025.7.0`). 항구 해소: [B-vault-upstream-migration](/ops/backlog) — dani-garcia/vaultwarden:1.36.0 native SSO ([D-ops-28](/ops/decisions)) |

## 다음 customer

- [신규 직원 등록](/ops/runbook/employee-onboarding)
- [Secret rotation](/ops/runbook/secret-rotation)
