feat(bootstrap): add HostEurope hardening playbook and workplan
Some checks failed
railiance-tests / smoke (push) Has been cancelled
Some checks failed
railiance-tests / smoke (push) Has been cancelled
- workplans/RAIL-BS-WP-0002-hosteurope-bootstrap.md: new workplan for Secure Single-Server Bootstrap at HostEurope (repo goal d7092599). T01-T03 done; T04+T05 require ansible on a box with network access to 92.205.62.239 (hosts.ini is gitignored — recreate on new box). - ansible/harden.yml: new playbook — disables root/password SSH auth, enables UFW (allow 22/tcp 6443/tcp 8472/udp, deny-all default), installs fail2ban with SSH jail, sets HISTCONTROL=ignorespace. - ansible/bootstrap.yml: import_playbook harden.yml runs before k3s. - ansible/hosts.ini.example: add [hosteurope] group template. - QUICKSTART.md: document two-stage bootstrap (harden → k3s). - CLAUDE.md: add goal_guidance handling to session protocol (needs_workplan + alignment_warnings). Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
15
CLAUDE.md
15
CLAUDE.md
@@ -37,8 +37,19 @@ Output a concise brief covering:
|
|||||||
any blocking decisions
|
any blocking decisions
|
||||||
2. **Pending tasks for this repo** — from local `workplans/` files (Step 2)
|
2. **Pending tasks for this repo** — from local `workplans/` files (Step 2)
|
||||||
plus any state hub tasks with `[repo:railiance-bootstrap]` in their title
|
plus any state hub tasks with `[repo:railiance-bootstrap]` in their title
|
||||||
3. **Suggested next action** — the highest-priority open item across both sources
|
3. **Goal guidance** — if the summary contains a `goal_guidance` key, act on it:
|
||||||
4. **SBOM status** — `last_sbom_at` for `railiance-bootstrap` is currently null
|
- **`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-<slug>.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`)
|
(gap: no lockfile yet — see `workplans/RAIL-BS-WP-0001-dependency-management.md`)
|
||||||
|
|
||||||
**During work:**
|
**During work:**
|
||||||
|
|||||||
@@ -122,7 +122,9 @@ From your local machine:
|
|||||||
ansible-playbook -i ansible/hosts.ini ansible/bootstrap.yml
|
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
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
- name: Railiance host bootstrap
|
||||||
hosts: all
|
hosts: all
|
||||||
become: true
|
become: true
|
||||||
|
|||||||
124
ansible/harden.yml
Normal file
124
ansible/harden.yml
Normal file
@@ -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
|
||||||
@@ -4,6 +4,10 @@
|
|||||||
[seed]
|
[seed]
|
||||||
# 203.0.113.10 ansible_user=ubuntu ansible_become=true
|
# 203.0.113.10 ansible_user=ubuntu ansible_become=true
|
||||||
|
|
||||||
|
# HostEurope server (second Kubernetes host)
|
||||||
|
[hosteurope]
|
||||||
|
# <IP> ansible_user=ubuntu ansible_become=true
|
||||||
|
|
||||||
# Optional: control plane / workers (future multi-node)
|
# Optional: control plane / workers (future multi-node)
|
||||||
#[k3s_master]
|
#[k3s_master]
|
||||||
# 203.0.113.11 ansible_user=ubuntu ansible_become=true
|
# 203.0.113.11 ansible_user=ubuntu ansible_become=true
|
||||||
|
|||||||
178
workplans/RAIL-BS-WP-0002-hosteurope-bootstrap.md
Normal file
178
workplans/RAIL-BS-WP-0002-hosteurope-bootstrap.md
Normal file
@@ -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`
|
||||||
Reference in New Issue
Block a user