docs(reimbursement-payment-batch-deployment): 记录受控部署验收
这个提交包含在:
@@ -19,6 +19,103 @@ operations require explicit approval and evidence.
|
||||
- Do not execute real payments, import bank statements, connect payment
|
||||
providers or write ERP without a separate approved Trellis task.
|
||||
|
||||
## Scenario: Existing Deployment SSH Identity Disambiguation
|
||||
|
||||
### 1. Scope / Trigger
|
||||
|
||||
- Trigger: `DEPLOY_SERVER` / `DEPLOY_REFRESH_SERVER` are not injected and local
|
||||
SSH configuration exposes more than one alias that can read the frozen
|
||||
deployment contract.
|
||||
- Purpose: select the existing least-privilege deployment account without
|
||||
guessing from alias names or accidentally publishing as unrestricted root.
|
||||
|
||||
### 2. Signatures
|
||||
|
||||
- Contract reader: `deployment_contract_load <ssh-target> <systemd-unit> <assoc-array>`.
|
||||
- Aggregate preflight:
|
||||
`DEPLOY_SERVER=<target> DEPLOY_REFRESH_SERVER=<target> bash deploy/trialrun/check-existing-deployment.sh`.
|
||||
- Refresh probe: `ssh <refresh-target> probe` must return exactly
|
||||
`[deployment-contract-refresh][PASS] probe`.
|
||||
- Deployment entry: `bash deploy/trialrun/deploy-existing.sh` with the two
|
||||
resolved targets kept only in process environment.
|
||||
|
||||
### 3. Contracts
|
||||
|
||||
- Never choose a target from its alias name, apparent hostname or ordering in
|
||||
SSH config, and never print candidate targets while probing.
|
||||
- A normal deployment candidate must be non-root, own the contracted
|
||||
`REMOTE_DIR`, `REMOTE_ENV_FILE` and synchronized source files, and have
|
||||
Docker access. These checks use numeric UID equality on the remote host.
|
||||
- An unrestricted root alias is not a valid normal deployment target even if
|
||||
it can read the same contract and pass the aggregate checks. Publishing as
|
||||
root can change synchronized-file ownership and break the intended
|
||||
least-privilege path.
|
||||
- Multiple aliases may point to the same machine and contract. Select a target
|
||||
automatically only when exactly one candidate satisfies the full non-root
|
||||
ownership contract; otherwise stop for explicit local configuration
|
||||
guidance.
|
||||
- The refresh identity is separate and valid only when the exact forced-command
|
||||
`probe` response matches. Do not reuse an unrestricted root login as the
|
||||
refresh identity.
|
||||
- After deployment, assert the source files still belong to the selected
|
||||
deployment UID before recording success.
|
||||
|
||||
### 4. Validation & Error Matrix
|
||||
|
||||
- No contract-readable candidate -> stop before remote mutation.
|
||||
- Exactly one non-root owner candidate + one exact refresh probe -> continue to
|
||||
13/13 aggregate preflight.
|
||||
- Several contract-readable aliases, but only one non-root owner -> use the
|
||||
owner candidate; do not treat alias count alone as ambiguity.
|
||||
- Several non-root owner candidates -> stop and request local SSH guidance;
|
||||
do not choose by config order or connection speed.
|
||||
- Root candidate passes 13/13 -> exclude it from normal deployment selection.
|
||||
- Post-deploy ownership differs from the selected UID -> fail closeout and
|
||||
investigate; do not report the release as clean.
|
||||
|
||||
### 5. Good/Base/Bad Cases
|
||||
|
||||
- Good: two aliases reach the same host and contract; the directory-owning
|
||||
non-root account is uniquely selected and ownership remains unchanged.
|
||||
- Base: one normal deployment identity and one forced-command refresh identity
|
||||
are injected explicitly; no discovery is needed.
|
||||
- Bad: use an unrestricted root alias because it also returns 13/13 PASS.
|
||||
- Bad: select the first matching `Host` entry or expose aliases, usernames,
|
||||
paths or SSH configuration in task evidence.
|
||||
|
||||
### 6. Tests Required
|
||||
|
||||
- Confirm the selected normal identity has a non-zero UID and numerically owns
|
||||
the contracted repository, environment file and a synchronized source file.
|
||||
- Confirm Docker access succeeds without sudo and the aggregate preflight is
|
||||
exactly `PASS=13 FAIL=0`.
|
||||
- Confirm the refresh identity returns the exact probe token and the normal
|
||||
identity is not substituted for it.
|
||||
- After release, compare the synchronized source owner UID with the selected
|
||||
identity UID and require equality.
|
||||
- Persist only PASS/FAIL counts and the selection rationale; run the sensitive
|
||||
content gate before commit.
|
||||
|
||||
### 7. Wrong vs Correct
|
||||
|
||||
#### Wrong
|
||||
|
||||
```bash
|
||||
# Both aliases connect, so choose the first one (which may be root).
|
||||
DEPLOY_SERVER="${candidates[0]}" bash deploy/trialrun/deploy-existing.sh
|
||||
```
|
||||
|
||||
#### Correct
|
||||
|
||||
```bash
|
||||
# Resolve exactly one non-root candidate that owns the frozen contract paths,
|
||||
# then run the aggregate gate before the controlled deployment wrapper.
|
||||
test "${#owner_candidates[@]}" -eq 1
|
||||
DEPLOY_SERVER="${owner_candidates[0]}" \
|
||||
DEPLOY_REFRESH_SERVER="$forced_refresh_target" \
|
||||
bash deploy/trialrun/check-existing-deployment.sh
|
||||
```
|
||||
|
||||
## Scenario: Deployment Env Contract
|
||||
|
||||
### 1. Scope / Trigger
|
||||
|
||||
在新工单中引用
屏蔽一个用户