diff --git a/WORK-RECORDS.md b/WORK-RECORDS.md index 8a43048..16c4f76 100644 --- a/WORK-RECORDS.md +++ b/WORK-RECORDS.md @@ -10,6 +10,7 @@ | --- | --- | --- | --- | --- | | workplan | RAIL-HO-WP-0006 | finished | — | workplans/RAIL-HO-WP-0006-forgejo-registry-ref-cleanup.md | | workplan | RAIL-HO-WP-0007 | finished | — | workplans/RAIL-HO-WP-0007-first-reef-rollout-and-s1-canonicalization.md | +| workplan | RAIL-HO-WP-0008 | ready | — | workplans/RAIL-HO-WP-0008-railiance01-resource-and-commercial-evidence.md | | task | RAIL-HO-WP-0006-T01 | done | — | workplans/RAIL-HO-WP-0006-forgejo-registry-ref-cleanup.md | | task | RAIL-HO-WP-0006-T02 | done | — | workplans/RAIL-HO-WP-0006-forgejo-registry-ref-cleanup.md | | task | RAIL-HO-WP-0006-T03 | done | — | workplans/RAIL-HO-WP-0006-forgejo-registry-ref-cleanup.md | @@ -23,3 +24,6 @@ | task | RAIL-HO-WP-0007-T02 | done | — | workplans/RAIL-HO-WP-0007-first-reef-rollout-and-s1-canonicalization.md | | task | RAIL-HO-WP-0007-T03 | done | — | workplans/RAIL-HO-WP-0007-first-reef-rollout-and-s1-canonicalization.md | | task | RAIL-HO-WP-0007-T04 | done | — | workplans/RAIL-HO-WP-0007-first-reef-rollout-and-s1-canonicalization.md | +| task | RAIL-HO-WP-0008-T01 | todo | — | workplans/RAIL-HO-WP-0008-railiance01-resource-and-commercial-evidence.md | +| task | RAIL-HO-WP-0008-T02 | todo | — | workplans/RAIL-HO-WP-0008-railiance01-resource-and-commercial-evidence.md | +| task | RAIL-HO-WP-0008-T03 | todo | — | workplans/RAIL-HO-WP-0008-railiance01-resource-and-commercial-evidence.md | diff --git a/ansible/inventory/group_vars/all.yaml b/ansible/inventory/group_vars/all.yaml index 13a6599..708913b 100644 --- a/ansible/inventory/group_vars/all.yaml +++ b/ansible/inventory/group_vars/all.yaml @@ -24,3 +24,29 @@ custodian_agent_pubkey: "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIC/V9fe5MGKdhTBz9Kw # Private key lives at ~/.ssh/id_ops on the workstation. Never commit the private key. ops_bridge_user: tegwick ops_bridge_pubkey: "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIKQmXbAVlEa8dzGx8Hk2S7AITpz6sMWdCN0MeMOzL82u ops-bridge@custodian" + +# --------------------------------------------------------------------------- +# k3s API firewall allowlist (6443/tcp) +# +# The k3s API is NOT world-reachable. Only these sources may connect. +# Added 2026-08-11 after discovering the live host was source-restricted by hand +# while this repo still declared 6443 open to Anywhere — meaning a convergence +# run would have exposed the Kubernetes API to the internet. +# +# These are operator egress addresses, not secrets. They are dynamic: when your +# ISP rotates the lease, MOVE the old entry to k3s_api_revoked_sources rather +# than deleting the line, so convergence prunes the stale grant instead of +# leaving it standing for whoever the address gets reassigned to. +# +# Durable alternative worth considering: reach the API over the ops-bridge SSH +# tunnel instead (see railiance-infra/docs/deploy-stack.md, the +# k3s-api-coulombcore pattern) and allow no public sources at all. +k3s_api_allowed_sources: + - address: "89.244.90.236" + comment: "k3s-api-operator-current" + +k3s_api_revoked_sources: + - address: "89.244.90.246" + comment: "rotated ISP lease, superseded 2026-08-11" + - address: "85.132.220.102" + comment: "historic operator address, retired" diff --git a/ansible/roles/base/defaults/main.yml b/ansible/roles/base/defaults/main.yml new file mode 100644 index 0000000..05c7f1b --- /dev/null +++ b/ansible/roles/base/defaults/main.yml @@ -0,0 +1,30 @@ +--- +# Base role defaults. + +# Source addresses permitted to reach the k3s API (6443/tcp). +# +# WHY THIS EXISTS +# --------------- +# The k3s API was originally opened to Anywhere by this role, and the source +# restriction was later added by hand on the live host. That left the declared +# config WEAKER than reality: re-running this role would have removed the +# restriction and exposed the Kubernetes API to the internet. Found 2026-08-11. +# +# The allowlist is therefore declared here and converged, not hand-edited. +# +# Deliberately empty by default. A host that sets no sources gets NO public +# access to 6443 — which is the safe failure. SSH (22) is unaffected, so a host +# converged with an empty list is always recoverable. +# +# Set the real values in inventory/group_vars/all.yaml. Each entry: +# - address: "203.0.113.10" +# comment: "k3s-api-operator-workstation" +k3s_api_allowed_sources: [] + +# Source addresses whose k3s API access must be REMOVED on convergence. +# +# Operator addresses rotate (dynamic ISP leases). Without this, every rotation +# leaves a standing grant to an address the ISP has since reassigned to someone +# else. Move an address here when it stops being yours; convergence then prunes +# it rather than leaving it to accumulate. +k3s_api_revoked_sources: [] diff --git a/ansible/roles/base/tasks/main.yml b/ansible/roles/base/tasks/main.yml index 19660a4..182d1c6 100644 --- a/ansible/roles/base/tasks/main.yml +++ b/ansible/roles/base/tasks/main.yml @@ -62,11 +62,48 @@ rule: allow name: OpenSSH -- name: Allow k3s API in UFW +# k3s API access is source-restricted. See roles/base/defaults/main.yml for why +# this is declared rather than hand-applied. Order matters below: grants are +# added BEFORE the blanket rule is removed, so convergence never opens a window +# in which the operator cannot reach the API. + +- name: Allow k3s API from approved operator sources only ansible.builtin.ufw: rule: allow port: '6443' proto: tcp + from_ip: "{{ item.address }}" + comment: "{{ item.comment | default('k3s-api-operator') }}" + loop: "{{ k3s_api_allowed_sources }}" + loop_control: + label: "{{ item.address }}" + +- name: Remove blanket k3s API rule if present (must not be world-reachable) + ansible.builtin.ufw: + rule: allow + port: '6443' + proto: tcp + delete: true + +- name: Revoke k3s API access for retired operator sources + ansible.builtin.ufw: + rule: allow + port: '6443' + proto: tcp + from_ip: "{{ item.address }}" + delete: true + loop: "{{ k3s_api_revoked_sources }}" + loop_control: + label: "{{ item.address }}" + +- name: Warn when no operator source is allowed to reach the k3s API + ansible.builtin.debug: + msg: >- + k3s_api_allowed_sources is empty, so 6443/tcp is closed to all external + sources on this host. This is the safe default, not an error. SSH is + unaffected and the host remains recoverable. Set the allowlist in + inventory/group_vars/all.yaml to restore API access. + when: k3s_api_allowed_sources | length == 0 - name: Allow Flannel VXLAN in UFW ansible.builtin.ufw: diff --git a/docs/verification.md b/docs/verification.md index 97aef90..cb36ad0 100644 --- a/docs/verification.md +++ b/docs/verification.md @@ -10,7 +10,11 @@ a reproducible, CI-friendly pass/fail verdict. of every managed node. It covers: - **Firewall** — UFW active, default deny inbound, required ports allowed - (SSH 22/tcp, k3s API 6443/tcp, Flannel VXLAN 8472/udp) + (SSH 22/tcp, Flannel VXLAN 8472/udp). The k3s API (6443/tcp) is + **source-restricted**, not world-open: only addresses in + `k3s_api_allowed_sources` may reach it, and `k3s_api_revoked_sources` is + pruned on convergence. A host with an empty allowlist has 6443 closed to all + external sources — the safe failure, recoverable over SSH. - **SSH daemon** — root login disabled, password auth disabled, pubkey auth enabled - **Services** — ufw, fail2ban, ssh.socket enabled and running - **Packages** — ufw, fail2ban, git, curl, vim, htop (age and sops installed as binaries) diff --git a/workplans/RAIL-HO-WP-0008-railiance01-resource-and-commercial-evidence.md b/workplans/RAIL-HO-WP-0008-railiance01-resource-and-commercial-evidence.md new file mode 100644 index 0000000..9e3cc40 --- /dev/null +++ b/workplans/RAIL-HO-WP-0008-railiance01-resource-and-commercial-evidence.md @@ -0,0 +1,81 @@ +--- +id: RAIL-HO-WP-0008 +type: workplan +title: "Publish railiance01 resource and commercial evidence" +domain: financials +repo: railiance-infra +status: ready +owner: codex +topic_slug: railiance +created: "2026-08-11" +updated: "2026-08-11" +related: + - RESOURCE-WP-0003 +state_hub_workstream_id: "7122657f-87c8-46b5-a725-a1af1ba0af12" +--- + +# RAIL-HO-WP-0008 — railiance01 resource and commercial evidence + +## Goal + +Provide the non-secret infrastructure facts needed to maintain +`resource:hosteurope:railiance01` in resource-control without making +resource-control authoritative for host provisioning or fin-hub's booked cost. +Origin: `RESOURCE-WP-0003-T04` delegated evidence gap. + +## T01 — Identify the provider resource and lifecycle + +```task +id: RAIL-HO-WP-0008-T01 +status: todo +priority: high +state_hub_task_id: "cde55584-229d-4e27-8fc8-0f54d78c71ed" +``` + +Record the Host Europe product/service class, non-secret provider resource ID, +account reference suitable for joins, country/region or residency evidence, +order or commissioning date, renewal date, cancellation deadline, and contract +term. Reference source evidence without committing invoices, credentials, +customer numbers, or payment instruments. + +Done when resource-control can update the provider and lifecycle fields with +provenance, while unknown or restricted fields remain explicitly classified. + +## T02 — Publish host capacity and utilization evidence + +```task +id: RAIL-HO-WP-0008-T02 +status: todo +priority: high +state_hub_task_id: "8b0e5547-23ae-4516-b6d7-9c196882d088" +``` + +Define a stable non-secret observation containing provisioned and usable CPU, +memory, root/local storage, traffic allowance where known, and timestamped +host-level utilization. Distinguish provider limits from operating-system and +Kubernetes observations. + +Done when at least one reproducible observation can be consumed by +resource-control and its source and cadence are documented. + +## T03 — Record infrastructure operations labor and exit inputs + +```task +id: RAIL-HO-WP-0008-T03 +status: todo +priority: medium +state_hub_task_id: "8bdaa96f-400b-455f-87db-d5c11e287142" +``` + +Define the recurring host-maintenance activities and measurable operator-labor +inputs, plus the host replacement/cancellation evidence required before exit. +Do not estimate another repository's workload labor. + +Done when resource-control can separate host infrastructure, recurring labor, +and migration effort in forecasts and optimization cases. + +## Acceptance + +- [ ] Provider identity and lifecycle evidence is non-secret and attributable. +- [ ] Capacity and utilization observations have timestamps and provenance. +- [ ] Host labor and exit inputs can be consumed without duplicating booked cost.