diff --git a/CLAUDE.md b/CLAUDE.md index 005c4ae..3bfb243 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -37,8 +37,19 @@ Output a concise brief covering: any blocking decisions 2. **Pending tasks for this repo** — from local `workplans/` files (Step 2) plus any state hub tasks with `[repo:railiance-bootstrap]` in their title -3. **Suggested next action** — the highest-priority open item across both sources -4. **SBOM status** — `last_sbom_at` for `railiance-bootstrap` is currently null +3. **Goal guidance** — if the summary contains a `goal_guidance` key, act on it: + - **`needs_workplan`** entries: for each active repo goal with no linked workstream, + surface it as the top suggested action — *"Repo goal '{title}' has no workplan yet. + Suggest: create workplans/RAIL-BS-WP-NNNN-.md and register a workstream + with repo_goal_id='{goal_id}'"*. Treat this as higher priority than continuing + existing work unless Bernd says otherwise. + - **`alignment_warnings`** entries: if active workstreams exist but are not linked + to the current repo goal, name the most recently active one and note: + *"Current work on '{recent_workstream_title}' may not be aligned with the active + goal '{active_goal_title}'. Continue unless you hear otherwise — but flag it."* +4. **Suggested next action** — the highest-priority open item across all sources, + with goal alignment taken into account +5. **SBOM status** — `last_sbom_at` for `railiance-bootstrap` is currently null (gap: no lockfile yet — see `workplans/RAIL-BS-WP-0001-dependency-management.md`) **During work:** diff --git a/QUICKSTART.md b/QUICKSTART.md index dd43a6a..5eff99c 100644 --- a/QUICKSTART.md +++ b/QUICKSTART.md @@ -122,7 +122,9 @@ From your local machine: ansible-playbook -i ansible/hosts.ini ansible/bootstrap.yml ``` -This installs prerequisites and sets up a single‑node k3s cluster. +This runs in two stages: +1. **Harden** — disables root/password SSH login, enables UFW (ports 22/6443/8472), installs fail2ban +2. **Bootstrap** — installs base packages and a single‑node k3s cluster --- diff --git a/ansible/bootstrap.yml b/ansible/bootstrap.yml index 7c7450f..e0d2101 100644 --- a/ansible/bootstrap.yml +++ b/ansible/bootstrap.yml @@ -1,4 +1,8 @@ --- +# Stage 1: Harden the server before anything else is installed. +- import_playbook: harden.yml + +# Stage 2: Install base packages and k3s. - name: Railiance host bootstrap hosts: all become: true diff --git a/ansible/harden.yml b/ansible/harden.yml new file mode 100644 index 0000000..917d4f6 --- /dev/null +++ b/ansible/harden.yml @@ -0,0 +1,124 @@ +--- +- name: Server hardening + hosts: all + become: true + + vars: + ssh_port: 22 + k3s_api_port: 6443 + flannel_vxlan_port: 8472 + + tasks: + # ── SSH hardening ──────────────────────────────────────────────────────── + + - name: Disable root SSH login + lineinfile: + path: /etc/ssh/sshd_config + regexp: '^#?PermitRootLogin' + line: 'PermitRootLogin no' + state: present + notify: Restart sshd + + - name: Disable password authentication + lineinfile: + path: /etc/ssh/sshd_config + regexp: '^#?PasswordAuthentication' + line: 'PasswordAuthentication no' + state: present + notify: Restart sshd + + - name: Disable challenge-response authentication + lineinfile: + path: /etc/ssh/sshd_config + regexp: '^#?ChallengeResponseAuthentication' + line: 'ChallengeResponseAuthentication no' + state: present + notify: Restart sshd + + # ── UFW firewall ───────────────────────────────────────────────────────── + + - name: Install ufw + apt: + name: ufw + state: present + update_cache: yes + + - name: Set UFW default inbound policy to deny + ufw: + default: deny + direction: incoming + + - name: Set UFW default outbound policy to allow + ufw: + default: allow + direction: outgoing + + - name: Allow SSH + ufw: + rule: allow + port: "{{ ssh_port }}" + proto: tcp + + - name: Allow k3s API server + ufw: + rule: allow + port: "{{ k3s_api_port }}" + proto: tcp + + - name: Allow Flannel VXLAN (inter-node) + ufw: + rule: allow + port: "{{ flannel_vxlan_port }}" + proto: udp + + - name: Enable UFW + ufw: + state: enabled + + # ── fail2ban ───────────────────────────────────────────────────────────── + + - name: Install fail2ban + apt: + name: fail2ban + state: present + + - name: Configure fail2ban SSH jail + copy: + dest: /etc/fail2ban/jail.d/sshd.conf + content: | + [sshd] + enabled = true + port = {{ ssh_port }} + maxretry = 5 + bantime = 3600 + findtime = 600 + mode: '0644' + notify: Restart fail2ban + + - name: Enable and start fail2ban + systemd: + name: fail2ban + enabled: true + state: started + + # ── Shell hygiene ───────────────────────────────────────────────────────── + + - name: Set HISTCONTROL to suppress space-prefixed commands from history + copy: + dest: /etc/profile.d/histcontrol.sh + content: | + # Commands prefixed with a space are not recorded in shell history. + # Use this when typing secrets interactively. + export HISTCONTROL=ignorespace + mode: '0644' + + handlers: + - name: Restart sshd + systemd: + name: sshd + state: restarted + + - name: Restart fail2ban + systemd: + name: fail2ban + state: restarted diff --git a/ansible/hosts.ini.example b/ansible/hosts.ini.example index 409f1ce..da2e399 100644 --- a/ansible/hosts.ini.example +++ b/ansible/hosts.ini.example @@ -4,6 +4,10 @@ [seed] # 203.0.113.10 ansible_user=ubuntu ansible_become=true +# HostEurope server (second Kubernetes host) +[hosteurope] +# ansible_user=ubuntu ansible_become=true + # Optional: control plane / workers (future multi-node) #[k3s_master] # 203.0.113.11 ansible_user=ubuntu ansible_become=true diff --git a/workplans/RAIL-BS-WP-0002-hosteurope-bootstrap.md b/workplans/RAIL-BS-WP-0002-hosteurope-bootstrap.md new file mode 100644 index 0000000..59c6d90 --- /dev/null +++ b/workplans/RAIL-BS-WP-0002-hosteurope-bootstrap.md @@ -0,0 +1,178 @@ +--- +id: RAIL-BS-WP-0002 +type: workplan +title: "Secure Single-Server Bootstrap at HostEurope" +domain: railiance +repo: railiance-bootstrap +status: active +owner: railiance +topic_slug: railiance +repo_goal_id: d7092599-927b-4796-b52e-8be833301478 +state_hub_workstream_id: "bf40b47e-be5b-4930-a7d2-362e76b943bb" +created: "2026-03-08" +updated: "2026-03-08" +handoff_note: "T01-T03 done locally (WSL, no ansible). T04+T05 require ansible on a Linux box with network access to 92.205.62.239. hosts.ini created with the IP (gitignored — recreate on new box). Run: ansible -i ansible/hosts.ini hosteurope -m ping, then ansible-playbook -i ansible/hosts.ini -l hosteurope ansible/bootstrap.yml" +--- + +# Secure Single-Server Bootstrap at HostEurope + +## Goal + +Bootstrap a new HostEurope server securely so that it can function as the +second Kubernetes host in the ThreePhoenix cluster. The bare Ubuntu server is +provisioned manually by Bernd. This workplan covers everything from first SSH +contact through a verified, hardened k3s node — all services secured from the +start, no shortcuts. + +Scope is deliberately narrow: one server, secure from day one. Automated +provisioning of additional server resources (railiance-hosts) and the +full three-node setup are deferred. + +## Boundary conditions + +- Ubuntu 24.04 LTS server at HostEurope, manually provisioned +- SSH access established using existing private credentials +- All remote access must be key-based only (no password auth) +- Firewall active before k3s is installed +- No credentials committed to the repo + +--- + +## Tasks + +### T01 — Add HostEurope host to inventory + +```task +id: RAIL-BS-WP-0002-T01 +status: done +completed: "2026-03-08" +priority: high +state_hub_task_id: "d41e1f8e-1dc2-41e9-bf83-521c99908e18" +``` + +Add the HostEurope host to `ansible/hosts.ini` (create from `hosts.ini.example` +if not present). Place it in a `[hosteurope]` group so it can be targeted +independently from other nodes. + +Verify Ansible can reach the host: + +```bash +ansible -i ansible/hosts.ini hosteurope -m ping +``` + +**Done when:** ping succeeds from local control node. + +--- + +### T02 — Create server hardening playbook + +```task +id: RAIL-BS-WP-0002-T02 +status: done +completed: "2026-03-08" +priority: high +state_hub_task_id: "e8abc64b-d74c-4718-9859-96d53551c654" +``` + +Create `ansible/harden.yml` (or a `roles/harden/` role called from +`bootstrap.yml`) covering: + +- Disable root SSH login (`PermitRootLogin no`) +- Disable password authentication (`PasswordAuthentication no`) +- Enable and configure UFW: deny all inbound by default, allow SSH (22), + k3s API (6443), and Flannel VXLAN (8472/UDP) — add others as needed +- Install and enable `fail2ban` with SSH jail +- Set `HISTCONTROL=ignorespace` in `/etc/profile.d/` to allow secret-safe + shell usage + +**Done when:** `ansible-lint ansible/harden.yml` passes and a dry-run +(`--check`) against the HostEurope host produces no errors. + +--- + +### T03 — Integrate hardening into bootstrap sequence + +```task +id: RAIL-BS-WP-0002-T03 +status: done +completed: "2026-03-08" +priority: high +state_hub_task_id: "bf991fa3-a870-4c80-8d9e-91f96b97eb7c" +``` + +Ensure `ansible/bootstrap.yml` runs hardening **before** k3s installation. +Either import `harden.yml` as a play or invoke the role inline. + +Order must be: +1. Harden (T02) +2. Install base packages +3. Install k3s + +Update `QUICKSTART.md` step 6 to note that hardening runs automatically as +part of bootstrap. + +**Done when:** `bootstrap.yml` task list reflects the correct order and the +QUICKSTART reflects it. + +--- + +### T04 — Run bootstrap on the HostEurope host + +```task +id: RAIL-BS-WP-0002-T04 +status: todo +priority: high +state_hub_task_id: "f62896f2-6fed-4512-a41b-3f5b9a1ca311" +``` + +Execute the full bootstrap playbook against the HostEurope host: + +```bash +ansible-playbook -i ansible/hosts.ini -l hosteurope ansible/bootstrap.yml +``` + +**Done when:** +- Playbook completes with no failed tasks +- `k3s kubectl get nodes` shows the host in `Ready` state +- UFW is active and SSH still works after the run + +--- + +### T05 — Smoke test and record + +```task +id: RAIL-BS-WP-0002-T05 +status: todo +priority: medium +state_hub_task_id: "9e92effe-3531-4675-8bc8-7b2bc2c04877" +``` + +Run the existing smoke test: + +```bash +tests/smoke_kube.sh +``` + +If the smoke test is not yet parameterized for a remote host, extend it or +run the equivalent `kubectl get nodes` via `ansible -m shell`. + +Add a brief note to `docs/` (or a new `docs/hosteurope-bootstrap.md`) recording: +- Server specs (vCPU, RAM, disk) +- IP / hostname (public, non-sensitive — no credentials) +- Date bootstrapped +- k3s version installed + +Call `add_progress_event()` in the State Hub to close out the workstream. + +**Done when:** smoke test passes and the progress event is logged. + +--- + +## References + +- Repo goal: `d7092599-927b-4796-b52e-8be833301478` +- ThreePhoenix workstream: `9e208376-23f1-40c7-9813-fac1f7d6ad3b` +- Safety Net workstream: `7e8b0c20-51eb-40c9-9e3b-85dd380d7625` (safety net + passed green 2026-02-27 — safe to proceed with cluster work) +- Existing playbook: `ansible/bootstrap.yml` +- Inventory template: `ansible/hosts.ini.example`