KeiSeiKit-1.0/skills/vm-provision/phase-2-plan.md
Parfii-bot eee5eecc20 feat(skills): /vm-provision 6-phase pipeline
Hub-and-spoke skill:
- SKILL.md (index) + phase-1-select-provider, phase-2-plan,
  phase-3-provision, phase-4-harden, phase-5-verify, phase-6-handoff.

Pipeline: select provider → Plan Mode doc → provision (hetzner/vultr
primitives, SSH first-contact TOFU) → harden-base.sh over SSH →
ssh-check + firewall-diff HARD GATE → artefact ledger + optional
/web-deploy handoff.

Invariants:
- ≥ 6 AskUserQuestion calls (Phase 1×2, 2×1, 3×1, 4×1, 5×1).
- Hard gate: Phase 6 refuses to run unless ssh-check AND firewall-diff
  both exit 0. "Ignore and proceed" is BLOCKED by design.
- RULE 0.8 (secrets ENV-ref only), RULE 0.4 (cite provider specifics),
  RULE 0.5 (plan.md written to <run-dir>/plan.md before provisioning),
  RULE -1 (every failure branch returns 2-3 constructive paths).

Defensive-only — no scanning tools, no CVE probes, no third-party
attack-surface analysis. Every phase file ≤ 200 LOC per Constructor
Pattern.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-21 21:00:14 +08:00

3.6 KiB

Phase 2 — Plan Mode Doc

Goal: produce a written, user-approved plan (RULE 0.5) that enumerates every apt change to the VM before any packet leaves the workstation. Verify criterion: user clicked "approve" on the plan; plan artefact exists at <run-dir>/plan.md.


2.a — Synthesise the plan

Write <run-dir>/plan.md (where <run-dir> is ./.keisei/vm-provision/<timestamp>/) with EXACTLY these sections — no more, no less:

# VM-Provision Plan — <timestamp>

## Intent
<INTENT one-line>

## Target
- Provider: <PROVIDER>
- Region:   <REGION>
- Plan:     <PLAN> (<arch>)
- VM name:  kei-<env>-<role>      # derived, ASK if ambiguous

## Access
- Admin user:  <ADMIN_USER>       # default keiadmin
- SSH port:    <SSH_PORT>         # default 22
- SSH pubkey:  <path>             # read from ~/.ssh/id_*.pub

## Ports to allow (ufw + provider cloud firewall)
<APP_PORTS  list>

## TLS
- Host:   <TLS_HOST or none>
- Method: <HTTP-01 | DNS-01 | none>

## Hardening steps (harden-base.sh)
- apt update + upgrade
- install: ufw fail2ban unattended-upgrades needrestart auditd audispd-plugins
- write /etc/ssh/sshd_config.d/99-kei.conf
- ufw default-deny-in + rate-limit ssh + allow APP_PORTS
- fail2ban sshd jail
- auditd baseline ruleset (/etc/audit/rules.d/99-kei.rules)
- unattended-upgrades (AUTO reboot = FALSE)

## Verification (hard gate before handoff)
- ssh-check  → exit 0
- firewall-diff (intent YAML vs live ufw) → exit 0

## Rollback
- `_primitives/provision-<provider>.sh destroy <VM_NAME>` — 1-command destroy.
- TF state: <path or "none  CLI-driven">

## Cost estimate
<Plan price per month from PROVIDER pricing page; CITE>

Cite the source for every price/region/plan detail. Numbers NOT cited = NO-GO per RULE 0.4.


2.b — Build the firewall-intent.yaml

Write <run-dir>/firewall-intent.yaml:

default:
  incoming: deny
  outgoing: allow
  routed: deny
rules:
  - port: <SSH_PORT>
    proto: tcp
    action: limit
    from: any
    comment: "ssh (rate-limited)"
  # one entry per APP_PORTS:
  - port: 443
    proto: tcp
    action: allow
    from: any

This file is the source of truth the Phase 5 firewall-diff will compare against live ufw status numbered output. Drift = Phase 5 fail.


2.c — AskUserQuestion (customise ports, TLS, admin name)

One AskUserQuestion call with up to 4 questions:

  1. Admin user? (stored as ADMIN_USER)

    • keiadmin (default)
    • Custom (user types — only free-text in Phase 2)
  2. SSH port? (stored as SSH_PORT)

    • 22 (default; simpler)
    • 2222 (obscurity; not security, but reduces log noise)
    • Custom
  3. Application ports to open? (multi-select, stored as APP_PORTS)

    • 443/tcp — HTTPS (most apps)
    • 80/tcp — HTTP (only if ACME HTTP-01 or redirect)
    • none — tunneled via Tailscale / private net only
  4. TLS? (stored as TLS_HOST + method)

    • Caddy HTTP-01 (need 80/tcp + 443/tcp + DNS pointing to VM)
    • Caddy DNS-01 (no port 80 needed; need DNS provider API token)
    • None (app provides its own TLS or is behind a proxy)

2.d — Present the plan for approval

Render plan.md in chat. Ask ONE final AskUserQuestion:

Proceed with this plan?

  • Approve → Phase 3.
  • Iterate → loop back to 2.c with the user's change request.
  • Abort → emit plan-only artefact and exit (HANDOFF_TO=none).

2.e — Verify criterion

  • plan.md written to <run-dir>/plan.md.
  • firewall-intent.yaml written to <run-dir>/firewall-intent.yaml.
  • User clicked "Approve".

Emit: Phase 2 done: plan @ <run-dir>/plan.md. <len(APP_PORTS)> ports, TLS=<method>.

Proceed to Phase 3.