<!-- task=m-principal-provider-credential-slot-routing/07+01,02,05_secret_material plan=1 tag=API milestone-task=secret-at-rest -->

# Provider credential secret material 보호 계획

## For the Implementing Agent

`CODE_REVIEW-*-G??.md`의 구현 담당 섹션은 반드시 채운다. 아래 검증을 실제로 실행하고 notes와 stdout/stderr를 기록한 뒤 active 파일을 유지한 채 review 준비 완료를 보고한다. 구현자는 사용자 질문, 상태 분류, archive, `complete.log` 작성을 수행하지 않는다.

## Background

Control Plane에는 credential envelope 스키마와 secret-blind 관리 서비스가 있지만 production sealer/key registry가 없다. 외부 secret-manager가 파일로 mount한 versioned AES-256-GCM keyring을 server composition에서 한 번만 로드하고, principal/slot/kind에 결합된 AAD로 restart와 key rotation 뒤에도 올바른 revision만 복호화되게 한다.

## Replan Evidence

- `plan_cloud_G08_0.log`와 `code_review_cloud_G09_0.log`는 최초 계획/미구현 review stub이다.
- 재검토에서 `credentialseal`이 상위 service package인 `credentialops.SecretContext`에 의존하도록 계획된 계층 역전과, expected-no-match `rg`가 전체 verification을 실패시키는 문제가 확인됐다.
- 구현 evidence와 verdict는 없으므로 기존 route를 승계하지 않고 isolated reassessment했다.

## Archive Evidence Snapshot

- `agent-task/archive/2026/08/m-principal-provider-credential-slot-routing/01_principal_store/complete.log`: principal/token durable ledger와 restart persistence PASS.
- `agent-task/archive/2026/08/m-principal-provider-credential-slot-routing/02+01_credential_catalog/complete.log`: slot/revision/envelope metadata와 lifecycle PASS.
- `agent-task/archive/2026/08/m-principal-provider-credential-slot-routing/05+01,02_management_core/complete.log`: principal-scoped management core, zeroization과 secret-blind failure surface PASS.
- prior unimplemented pair: `plan_cloud_G08_0.log`, `code_review_cloud_G09_0.log`.

## Roadmap Targets

- Milestone: `agent-roadmap/phase/operational-observability-provider-management/milestones/principal-provider-credential-slot-routing.md`
- Milestone link: [Milestone 문서](agent-roadmap/phase/operational-observability-provider-management/milestones/principal-provider-credential-slot-routing.md)
- Task ids:
  - `secret-at-rest`: provider raw token envelope encryption과 외부 key source
- Completion mode: check-on-pass

## Analysis

### Files Read

- `agent-roadmap/phase/operational-observability-provider-management/milestones/principal-provider-credential-slot-routing.md`
- `agent-roadmap/sdd/operational-observability-provider-management/principal-provider-credential-slot-routing/SDD.md`
- `apps/control-plane/internal/credentialstore/secret_envelope.go`
- `apps/control-plane/internal/credentialstore/slot.go`
- `apps/control-plane/internal/credentialstore/slot_test.go`
- `apps/control-plane/internal/credentialops/service.go`
- `apps/control-plane/internal/credentialops/service_test.go`
- `apps/control-plane/cmd/control-plane/main.go`
- `apps/control-plane/cmd/control-plane/server.go`
- `apps/control-plane/cmd/control-plane/credential_commands.go`
- `apps/control-plane/cmd/control-plane/config_test.go`
- `apps/control-plane/cmd/control-plane/credential_store_test.go`
- `configs/control-plane.yaml`
- `agent-test/local/rules.md`
- `agent-test/local/control-plane-smoke.md`

### SDD Criteria

- SDD path: `agent-roadmap/sdd/operational-observability-provider-management/principal-provider-credential-slot-routing/SDD.md`; status: 승인.
- Acceptance Scenario S09/`secret-at-rest`와 Evidence Map S09-S13의 encrypted-store inspection row를 대상으로 한다.
- DB에는 algorithm/key id/version/nonce/ciphertext/AAD만 남고, 올바른 key revision과 exact principal/slot/kind context에서만 decrypt되어야 한다.
- 이 row가 keyring, slot-bound AAD, restart/key-version test와 final leak scan을 checklist/verification에 직접 고정한다.

### Verification Context

- 별도 handoff는 없었다. repository-native fallback evidence는 위 source/tests와 `agent-test/local/rules.md`, `agent-test/local/control-plane-smoke.md`다.
- 현재 host는 Linux/aarch64, Go `1.26.2`; 외부 provider 호출 없이 test-owned temp key file과 SQLite로 검증한다.
- external key source는 특정 KMS SDK가 아니라 deployment secret-manager가 `0600` 파일로 mount하는 계약이다. 파일 내용·key 값·raw secret은 evidence에 기록하지 않는다.
- `-count=1`, SQLite reopen, fresh process startup test를 사용한다. confidence: high.

### Test Coverage Gaps

- 기존 tests는 fake sealer/registry만 사용해 AES-GCM, nonce, tamper, wrong AAD/key revision을 검증하지 않는다.
- slot ID가 seal 뒤 store에서 생성되어 create-time AAD를 stable slot에 결합할 수 없다.
- startup/CLI는 production keyring을 조립하지 않으며 missing/permission/length/duplicate/restart test가 없다.

### Symbol References

- `credentialops.SecretContext`를 `credentialseal.Context`로 대체한다. `credentialops`가 하위 crypto package를 의존하고 `credentialseal`은 `credentialops`를 import하지 않아 순환/계층 역전을 막는다.
- `credentialstore.CreateSlotInput.SlotID`는 additive optional field다. 기존 zero-value call site는 store-generated ID로 호환한다.

### Split Judgment

- stable contract는 외부 key material, slot-bound authenticated envelope, restart-safe open과 raw-secret 비관측이다.
- 01/02/05 predecessor는 위 exact `complete.log`로 충족됐다. 08은 이 packet의 `Open`과 envelope context를 사용한다.

### Scope Rationale

- TLS, runtime lease, Node injection, migration과 attribution은 08~10의 소유다.
- tracked YAML에는 mount path/key id/version placeholder만 둔다. cloud KMS/Vault SDK 선택은 제품 결정이 없어 제외한다.
- 기존 unrelated/user changes는 수정하지 않는다.

### Final Routing

- evaluation_mode: `isolated-reassessment`; finalizer: `finalize-task-policy.sh pair` exactly once.
- build closures 모두 true; scores `2,2,2,1,1` = G08; base `local-fit`, risks `temporal_state`, `concurrent_consistency`, `boundary_contract`, `variant_product` (4); route `risk-boundary`, `PLAN-cloud-G08.md`.
- review scores `2,2,2,1,2` = G09; route `official-review`, `CODE_REVIEW-cloud-G09.md` (Codex `gpt-5.6-sol`, xhigh).
- `large_indivisible_context=false`, `review_rework_count=0`, `evidence_integrity_failure=false`; recovery boundary는 없다. capability gap도 없다.

## Implementation Checklist

- [ ] Implement a versioned AES-256-GCM keyring with a lower-layer context type, canonical AAD, external mount loading, decrypt support, and cleanup.
- [ ] Allocate a stable slot id before sealing and preserve transactional create/rotate CAS and failure-no-mutation behavior.
- [ ] Wire one keyring instance into Control Plane server/store/service composition with fail-closed startup while keeping principal bootstrap store-only.
- [ ] Add fresh crypto, key-version, restart, DB inspection, permissions, and secret-leak tests.
- [ ] Fill implementation-owned sections in CODE_REVIEW-*-G??.md with actual implementation notes and verification output.

### [API-1] Versioned authenticated keyring without layer inversion

**Problem:** `credentialstore.SecretEnvelope` validates only presence and `credentialops.SecretSealer` has no production implementation. The prior plan made the new crypto package depend on `credentialops.SecretContext`, coupling a lower-level primitive to a service package.

**Solution:** Add `credentialseal.Context`, `Sealer`, and `Keyring` in the crypto package. Load a strict YAML manifest from an operator-supplied file, resolve secret-manager atomic symlinks once, and verify the resolved target is a regular file owned by the current user/root with no group/world access; reject dangling/changing/non-regular targets, empty material, duplicate id/version, non-positive version, and keys not exactly 32 decoded bytes. Use AES-256-GCM, a fresh random nonce, canonical length-delimited AAD over principal/slot/kind, copied outputs, constant non-sensitive errors, and best-effort zeroing of decoded/plaintext buffers. `credentialops.SecretSealer` consumes `credentialseal.Context`; `credentialseal` may depend on the data-only `credentialstore.SecretEnvelope` but never imports `credentialops`.

Before (`apps/control-plane/internal/credentialops/service.go:41-51`):

```go
type SecretSealer interface {
	Seal(ctx context.Context, plaintext []byte, ctx_ SecretContext) (credentialstore.SecretEnvelope, error)
}
type SecretContext struct {
	PrincipalID string
	SlotID      string
	Kind        string
}
```

After:

```go
import "iop/apps/control-plane/internal/credentialseal"

type SecretSealer interface {
	Seal(context.Context, []byte, credentialseal.Context) (credentialstore.SecretEnvelope, error)
}
```

**Modified Files and Checklist:**

- [ ] Add `apps/control-plane/internal/credentialseal/aead.go` and `apps/control-plane/internal/credentialseal/aead_test.go`.
- [ ] Tighten `apps/control-plane/internal/credentialstore/secret_envelope.go` for algorithm, nonce, AAD and key revision validation.
- [ ] Update `apps/control-plane/internal/credentialops/service.go` and tests to use the lower-layer context.

**Test Strategy:** round-trip/restart, nonce uniqueness, wrong context, ciphertext/AAD/nonce tamper, unknown version, valid atomic symlink, dangling/swap/non-regular/insecure-target manifest, and error/string secret absence.

**Verification:** `go test -count=1 ./apps/control-plane/internal/credentialseal ./apps/control-plane/internal/credentialstore ./apps/control-plane/internal/credentialops`.

### [API-2] Slot-bound seal and durable revision inspection

**Problem:** `credentialstore.CreateSlot` currently generates the slot ID after the service has sealed the secret.

**Solution:** Permit a validated caller-generated UUID in `CreateSlotInput`, keep store generation for legacy zero value, and have `credentialops.Service.CreateSlot` generate the UUID before sealing. Rotation obtains principal/slot/kind from the current row, seals for that exact context, then performs the existing revision CAS. No failed crypto/validation step may create or advance rows.

Before (`apps/control-plane/internal/credentialstore/slot.go:54-61`):

```go
type CreateSlotInput struct {
	PrincipalID string
	Vendor string
	CredentialKind string
	Alias string
	Envelope SecretEnvelope
}
```

After:

```go
type CreateSlotInput struct {
	SlotID string
	PrincipalID string
	Vendor string
	CredentialKind string
	Alias string
	Envelope SecretEnvelope
}
```

**Modified Files and Checklist:**

- [ ] Update `apps/control-plane/internal/credentialstore/slot.go` and `apps/control-plane/internal/credentialstore/slot_test.go`.
- [ ] Update `apps/control-plane/internal/credentialops/service.go` and `apps/control-plane/internal/credentialops/service_test.go` for create/rotate context and non-leaking failure mapping.

**Test Strategy:** inspect current/history SQLite rows for ciphertext-only storage, reopen DB/keyring, decrypt supported revisions with exact context, reject swapped slot/principal/kind, and assert failed seal/open leaves counts/revisions unchanged.

**Verification:** `go test -count=1 ./apps/control-plane/internal/credentialstore ./apps/control-plane/internal/credentialops`.

### [API-3] Production server/store/service composition

**Problem:** `server.go` opens the store without a registry or concrete secret service composition. The only existing host-local command is first-principal bootstrap and does not touch provider secret envelopes.

**Solution:** Add `credential_encryption.key_file`, `active_key_id`, and `active_key_version`. When all three are omitted, preserve the existing principal/metadata store and inject no sealer so every provider-secret mutation remains fail-closed. When any field is present, require the complete triple, load the keyring before listeners, inject one instance into store and `credentialops.Service`, and expose the service only to later secure handlers through explicit composition options. Partial/invalid config fails before listeners; a non-empty database URL alone remains compatible. 08's credential-plane activation will require the complete encryption config. First-principal bootstrap remains store-only and does not unnecessarily load encryption keys.

Before (`apps/control-plane/cmd/control-plane/server.go:31-39`):

```go
store, err = credentialstore.Open(ctx, cfg.Database.URL)
if err != nil {
	return fmt.Errorf("open credential store: %w", err)
}
```

After:

```go
import "iop/apps/control-plane/internal/credentialseal"

keyring, err := credentialseal.LoadFile(cfg.CredentialEncryption)
if err != nil { return fmt.Errorf("load credential encryption: %w", err) }
store, err = credentialstore.Open(ctx, cfg.Database.URL,
	credentialstore.WithEnvelopeKeyRegistry(keyring))
```

**Modified Files and Checklist:**

- [ ] Update `apps/control-plane/cmd/control-plane/main.go` and `apps/control-plane/cmd/control-plane/server.go` with one explicit credential runtime composition helper.
- [ ] Update `apps/control-plane/cmd/control-plane/config_test.go` and `apps/control-plane/cmd/control-plane/credential_store_test.go` for keyring/service injection and bootstrap non-regression.
- [ ] Update `configs/control-plane.yaml` with secret-free placeholders only.

**Test Strategy:** temp mounted files and an atomic-symlink fixture cover database-only compatibility, all-fields-omitted secret-disabled mode, each partial triple, configured success, missing/permission/malformed/unknown active key, restart reopen, listener-not-started on error, and logs/config formatting without path contents, key bytes, plaintext, or ciphertext.

**Verification:** `go test -count=1 ./apps/control-plane/cmd/control-plane`.

## Dependencies and Execution Order

1. 01, 02, 05 predecessors are satisfied by the exact archive evidence above.
2. Implement API-1, API-2, API-3 in order.
3. Do not start `08+03,04,07_secure_delivery` until this directory has `complete.log`.

## Modified Files Summary

| File | Items |
|------|-------|
| `apps/control-plane/internal/credentialseal/aead.go` | API-1 |
| `apps/control-plane/internal/credentialseal/aead_test.go` | API-1 |
| `apps/control-plane/internal/credentialstore/secret_envelope.go` | API-1 |
| `apps/control-plane/internal/credentialstore/slot.go` | API-2 |
| `apps/control-plane/internal/credentialstore/slot_test.go` | API-2 |
| `apps/control-plane/internal/credentialops/service.go` | API-1, API-2 |
| `apps/control-plane/internal/credentialops/service_test.go` | API-1, API-2 |
| `apps/control-plane/cmd/control-plane/main.go` | API-3 |
| `apps/control-plane/cmd/control-plane/server.go` | API-3 |
| `apps/control-plane/cmd/control-plane/config_test.go` | API-3 |
| `apps/control-plane/cmd/control-plane/credential_store_test.go` | API-3 |
| `configs/control-plane.yaml` | API-3 |
| `agent-task/m-principal-provider-credential-slot-routing/07+01,02,05_secret_material/CODE_REVIEW-cloud-G09.md` | all |

## Final Verification

Run from `/config/workspace/iop-s0`; cached results are not accepted.

```bash
command -v go && go version && go env GOROOT
git status --short --branch
go test -count=1 ./apps/control-plane/internal/credentialseal ./apps/control-plane/internal/credentialstore ./apps/control-plane/internal/credentialops
go test -count=1 ./apps/control-plane/cmd/control-plane
go test -count=1 ./apps/control-plane/...
go vet ./apps/control-plane/...
git diff --check
credential_leak_pattern='BEGIN'' AGE PRIVATE KEY|active_key_value[[:space:]]*:[[:space:]]*[^<$]'
if rg --sort path -n --hidden --glob '!agent-task/archive/**' --glob '!*.sops.yaml' --glob '!PLAN-*.md' --glob '!*.log' "$credential_leak_pattern" apps/control-plane configs agent-task/m-principal-provider-credential-slot-routing/07+01,02,05_secret_material; then exit 1; fi
```

Expected: all commands exit 0; the guarded scan emits no match. Review evidence records actual stdout/stderr without secret values.

After completing all code changes, fill implementation-owned sections in `CODE_REVIEW-*-G??.md`.
