# sealed key 공급 및 launcher_fn 결선 설계

날짜: 2026-06-07  
작성자: 토르 (개발2팀 백엔드)  
작업 번호: task-2729+13  
보안 레벨: Lv.3 (보안 민감)

---

## 배경

sealed key 공급 경로는 이미 SATISFIED 상태다.
- `.env.keys` 에 `COKACDIR_KEY_ANU` 환경변수로 런타임 주입.
- `_default_sealed_key_loader()` (anu_result_pickup_runner)가 해당 환경변수를 읽어 반환. 부재 시 None 반환(fail-closed). raw key 파일 기록 없음.

남은 유일한 gap: `main()` 에서 `scan_once` 호출 시 `launcher_fn=None` 으로 고정되어 있어 real auto-wake 경로가 실질적으로 차단된 상태였음. `process_one` / `scan_once` 는 이미 `launcher_fn` 인자 전달 구조 완비. 미결선 지점만 `main()` 이었다.

---

## sealed key 공급 방식 후보 비교

- A) `.env.keys` COKACDIR_KEY_ANU (현 로더 기본)
  - 위험: 파일 노출 시 키 평문 유출 가능. 반드시 0600 퍼미션 + git 미포함 강제.
  - 노출면: 단일 파일. 프로세스 환경 변수(ps aux / /proc/$pid/environ 경유 열람 위험).
  - 회전: .env.keys 수정 + 프로세스 재기동으로 즉시 반영.
  - rollback: 이전 키 값으로 .env.keys 복구 후 재기동.

- B) systemd EnvironmentFile=(0600 파일)
  - 위험: EnvironmentFile 경로 노출 시 파일 직접 열람 가능. systemd 유닛 파일 자체는 0644 일 수 있음.
  - 노출면: 파일시스템 + journald 환경변수 로그. `systemctl show` 로 env 노출 주의.
  - 회전: EnvironmentFile 수정 + `systemctl daemon-reload` + 서비스 재기동.
  - rollback: 이전 내용 복원 후 reload. 단, 이미 실행된 프로세스는 구 키 유지.

- C) 외부 secret store → env injection (Vault, AWS SSM, GCP Secret Manager 등)
  - 위험: secret store 접근 자격(token/role)이 새로운 공격 표면. 네트워크 의존성 추가.
  - 노출면: 접근 로그 남음. injection 스크립트/래퍼가 env 를 자식 프로세스로 전달하는 방식에 따라 노출 범위 다름.
  - 회전: secret store 내 버전 rotation 으로 무중단 가능. 클라이언트 재기동 없이 교체 가능(동적 시크릿).
  - rollback: 이전 버전 활성화 후 클라이언트 갱신(또는 캐시 만료 대기).

현 구현은 A) 방식 채택. B·C 는 운영 성숙 단계에서 고려.

---

## wiring 설계

호출 흐름:

  main()
    └─ build_launcher_fn(CANONICAL_ROOT)
         ├─ read_real_wake_enabled() → False  →  None 반환 (flag 부재 기본)
         └─ (flag on 시) _default_sealed_key_loader() → sealed_key
              └─ _verifier = hmac.compare_digest 기반 상수시간 비교
              └─ functools.partial(launch_wake, dry_run=False, anu_key_verifier=_verifier) 반환
    └─ scan_once(CANONICAL_ROOT, legacy_cutoff=True, launcher_fn=launcher_fn)
         └─ process_one(..., launcher_fn=launcher_fn)
              └─ (WAKE_BUILT 시) launcher_fn(argv, task_id=..., sha256=...) 호출

flag 부재(기본 프로덕션 상태) 시 `build_launcher_fn` 은 None 반환.
`scan_once(launcher_fn=None)` → `process_one(launcher_fn=None)` → launcher 호출 없음 → 현행 동작과 100% 동일.

---

## real-wake flag

- 경로: `memory/state/p0b_real_wake_enabled`
- 활성 조건: 파일 첫 줄 trim 값이 정확히 `"enabled"` 일 때만 True.
- 부재가 기본 (ACTIVE = false). 파일이 없으면 read_real_wake_enabled() → False.
- 부재/읽기실패/그 외 임의 문자열 → False (fail-closed). 예외 catch 포함.
- 이 파일은 코드가 읽기만 함. 코드에서 생성 절대 금지.

---

## fail-closed 매트릭스

- real-wake flag 파일 부재 또는 값 != "enabled"
  → build_launcher_fn() = None → scan_once(launcher_fn=None) → real wake 0. 현행 보존.

- real-wake flag = "enabled" 이나 sealed key 부재(COKACDIR_KEY_ANU 미설정 또는 None/빈값)
  → build_launcher_fn() = None → real wake 0.

- real-wake flag = "enabled" + sealed key 존재 + verifier 검증 실패
  (candidate != sealed_key, 또는 non-string 타입, 또는 compare_digest 예외)
  → _verifier() = False → launch_wake 내부 step3: anu_key_verifier 검증 실패
  → FAIL_CLOSED_NON_ANU_KEY (wake 0). 이미 launch_wake 가 처리.

- real-wake flag = "enabled" + sealed key 존재 + anu_key_verifier non-callable 또는 예외
  → launch_wake 내부 FAIL_CLOSED_NON_ANU_KEY (wake 0). 이미 launch_wake 가 처리.

- build_launcher_fn 내 loader() 예외
  → except Exception → None 반환 → real wake 0.

---

## 3중 차단 보존

real spawn 을 발생시키려면 다음 3개 조건이 모두 충족되어야 한다:

  1. p0b_driver_enabled == "enabled"  (감지 활성화 — scan_once 진입 허용)
  2. p0b_real_wake_enabled == "enabled"  (real wake 결선 허용)
  3. COKACDIR_KEY_ANU 환경변수 존재 + sealed key 일치  (verifier 통과)

systemd enable 만으로는 조건 2·3 이 충족되지 않으므로 real spawn 0.
조건 1만으로는 surface scan 만 실행(launcher_fn=None → process_one 내 launch 경로 비활성).
이 설계로 기존 3중 차단 구조를 완전히 보존한다.

---

## 수정 범위

수정 파일: `dispatch/anu_pickup_driver.py` 1개만.
read-only 참조 모듈: `anu_pickup_wake_launcher.py`, `anu_result_pickup_runner.py` (수정 없음).
memory/state/** 파일 생성 없음. .env.keys 키 값 기록 없음.
