Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions ansible/roles/anvil/handlers/main.yml
Original file line number Diff line number Diff line change
Expand Up @@ -7,3 +7,6 @@
ansible.builtin.systemd:
name: anvil
state: restarted
# Under --check on a fresh host the unit isn't installed yet; flush_handlers would
# otherwise fail trying to restart a non-existent service.
when: not ansible_check_mode or 'anvil.service' in (ansible_facts.services | default({}))
33 changes: 31 additions & 2 deletions ansible/roles/anvil/tasks/main.yml
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,23 @@
create_home: true
shell: /usr/sbin/nologin

# Under --check the user/apt/Foundry tasks below are no-ops, so on a fresh host the
# anvil user and Foundry binaries don't exist yet — become_user, cast, and the
# systemd unit would all hard-fail. Probe real on-host state (read-only, runs in
# check mode) so those tasks skip in check mode while still previewing drift on an
# already-provisioned host.
- name: Probe whether the anvil user already exists on the target
ansible.builtin.getent:
database: passwd
key: "{{ anvil_user }}"
fail_key: false
Comment thread
thiras marked this conversation as resolved.
Comment thread
thiras marked this conversation as resolved.

# Service guards below reuse baseline's service_facts; gather here too so the role
# stays correct when run standalone (e.g. --tags anvil) without baseline first.
- name: Probe service facts if not already gathered
ansible.builtin.service_facts:
when: ansible_facts.services is not defined

- name: Install Foundry bootstrap prerequisites
ansible.builtin.apt:
name: [curl, git, jq, ca-certificates]
Expand Down Expand Up @@ -52,7 +69,11 @@
executable: /bin/bash
environment:
FOUNDRY_DIR: "{{ foundry_dir }}"
when: not anvil_bin.stat.exists
when:
- not anvil_bin.stat.exists
# become_user requires the anvil user to exist; --check didn't create it.
# fail_key:false stores a missing user as {user: None}, so test the value.
- not ansible_check_mode or (getent_passwd | default({})).get(anvil_user)
Comment thread
thiras marked this conversation as resolved.

- name: Ensure /etc/anvil exists
ansible.builtin.file:
Expand All @@ -69,7 +90,10 @@
register: anvil_env_stat

- name: Generate and install the shared mnemonic
when: not anvil_env_stat.stat.exists
when:
- not anvil_env_stat.stat.exists
# cast lives in the Foundry bin; under --check on a fresh host it isn't installed.
- not ansible_check_mode or anvil_bin.stat.exists
block:
- name: Generate a fresh mnemonic with cast
ansible.builtin.command:
Expand Down Expand Up @@ -135,10 +159,11 @@
name: anvil
enabled: true
state: started
when: not ansible_check_mode or 'anvil.service' in (ansible_facts.services | default({}))

- name: Wait for the anvil RPC to answer on loopback
ansible.builtin.uri:
url: "http://{{ anvil_host }}:{{ anvil_port }}"

Check warning on line 166 in ansible/roles/anvil/tasks/main.yml

View workflow job for this annotation

GitHub Actions / kics

[MEDIUM] Communication Over HTTP

Using HTTP URLs (without encryption) could lead to security vulnerabilities and risks
method: POST
body_format: json
body:
Expand All @@ -155,3 +180,7 @@
# Assert it's OUR chain, not a stale process on the port answering 200.
- (anvil_rpc_probe.json.result | default('0x0')) | int(0, 16) == anvil_chain_id
changed_when: false
# Skip entirely in check mode: --check never (re)starts anvil, so this liveness
# probe would falsely fail against a stopped-but-provisioned host. It previews no
# drift (changed_when:false) — there is nothing to verify in a dry run.
when: not ansible_check_mode
6 changes: 6 additions & 0 deletions ansible/roles/baseline/handlers/main.yml
Original file line number Diff line number Diff line change
@@ -1,10 +1,16 @@
---
# Both units come from baseline_packages, a no-op under --check, so on a fresh host
# the unit may not exist when a flush (e.g. anvil's flush_handlers) fires the handler.
# Guard on real on-host state (service_facts, gathered in tasks/main.yml): run on a
# provisioned host (drift preview), skip a missing unit in check mode.
- name: Reload nftables
ansible.builtin.systemd:
name: nftables
state: restarted
when: not ansible_check_mode or 'nftables.service' in (ansible_facts.services | default({}))

- name: Restart fail2ban
ansible.builtin.systemd:
name: fail2ban
state: restarted
when: not ansible_check_mode or 'fail2ban.service' in (ansible_facts.services | default({}))
26 changes: 26 additions & 0 deletions ansible/roles/baseline/tasks/main.yml
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,14 @@
update_cache: true
cache_valid_time: 3600

# Under --check the install above is a no-op, so packaged units (fail2ban, chrony,
# nftables) may not exist yet on a fresh host and a systemd enable/start would
# hard-fail ("Could not find the requested service"). Probe real on-host state
# (read-only, runs in check mode) so the service tasks below can skip a missing
# unit in check mode while still previewing drift on an already-provisioned host.
- name: Probe which service units already exist on the target
ansible.builtin.service_facts:

# --- Admin account (must exist + have a key BEFORE ssh_hardening locks root) ---
# Resolve the user and key set from the control machine when left empty; explicit
# values always win. Lookups run on the control node — that's the operator's box.
Expand Down Expand Up @@ -87,12 +95,27 @@
shell: /bin/bash
create_home: true

# authorized_key resolves the user's ~/.ssh to write the key, so the account must
# already exist. Under --check the create task above is a no-op, so on a not-yet-
# provisioned host the user is absent and the module hard-fails ("Either user must
# exist or you must provide full path to key file in check mode"). Probe real
# on-host state (read-only, runs in check mode) and skip the key install only when
# check mode meets a missing user; real runs and check runs on a provisioned host
# (where drift is worth previewing) always proceed.
- name: Probe whether the admin user already exists on the target
ansible.builtin.getent:
database: passwd
key: "{{ ssh_admin_user_effective }}"
fail_key: false

- name: Install admin authorized keys
ansible.posix.authorized_key:
user: "{{ ssh_admin_user_effective }}"
key: "{{ item }}"
state: present
loop: "{{ ssh_admin_keys }}"
# fail_key:false stores a missing user as {user: None}, so test the value, not `in`.
when: not ansible_check_mode or (getent_passwd | default({})).get(ssh_admin_user_effective)

# --- Firewall: default-deny inbound, SSH only ---------------------------------
- name: Install nftables ruleset
Expand All @@ -110,6 +133,7 @@
name: nftables
enabled: true
state: started
when: not ansible_check_mode or 'nftables.service' in (ansible_facts.services | default({}))

# --- Automatic security patches ----------------------------------------------
- name: Enable unattended-upgrades
Expand Down Expand Up @@ -140,13 +164,15 @@
name: fail2ban
enabled: true
state: started
when: not ansible_check_mode or 'fail2ban.service' in (ansible_facts.services | default({}))

# --- Time sync ----------------------------------------------------------------
- name: Enable and start chrony
ansible.builtin.systemd:
name: chrony
enabled: true
state: started
when: not ansible_check_mode or 'chrony.service' in (ansible_facts.services | default({}))

# --- DevSec hardening (LAST: ssh_hardening disables root + password auth) ------
- name: Apply DevSec OS hardening
Expand Down
3 changes: 3 additions & 0 deletions ansible/roles/caddy/handlers/main.yml
Original file line number Diff line number Diff line change
Expand Up @@ -5,3 +5,6 @@
ansible.builtin.systemd:
name: caddy
state: restarted
# Under --check on a fresh host the unit isn't installed yet; flush_handlers would
# otherwise fail trying to restart a non-existent service.
when: not ansible_check_mode or 'caddy.service' in (ansible_facts.services | default({}))
9 changes: 8 additions & 1 deletion ansible/roles/caddy/tasks/add_user.yml
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,14 @@
failed_when: false

- name: Mint '{{ caddy_target_user }}'
when: caddy_user_check.rc != 0
# Never mint in check mode: minting needs the caddy binary (hash-password, which
# auto-skips under --check), writes the TSV, and reveals a password — all mutating
# side effects a dry run must not perform. Gating on ansible_check_mode first also
# means caddy_user_check.rc — undefined when the existence-check command auto-skips
# under --check — is never the deciding factor; default it so it can't error.
when:
- not ansible_check_mode
- caddy_user_check.rc | default(1) != 0
block:
- name: Generate a random password
ansible.builtin.set_fact:
Expand Down
22 changes: 22 additions & 0 deletions ansible/roles/caddy/tasks/main.yml
Original file line number Diff line number Diff line change
Expand Up @@ -25,11 +25,28 @@

# Rebuild the Caddyfile import block from the TSV registry. Idempotent: identical
# registry -> identical content -> no change -> no restart.
# Under --check the user-mint tasks don't write (the existence probe is a command
# that auto-skips; the lineinfile registry write is a no-op), so on a fresh host the
# TSV registry is never created and the slurp below would fail (file not found).
Comment thread
thiras marked this conversation as resolved.
# Probe for it (read-only, runs in check mode); the slurp/render skip in check mode
# when it's absent, but still preview drift on an already-provisioned host.
- name: Check whether the basic-auth registry exists
ansible.builtin.stat:
path: "{{ caddy_users_tsv }}"
register: caddy_tsv_stat
Comment thread
thiras marked this conversation as resolved.

# Service guards below reuse baseline's service_facts; gather here too so the role
# stays correct when run standalone (e.g. --tags caddy) without baseline first.
- name: Probe service facts if not already gathered
ansible.builtin.service_facts:
when: ansible_facts.services is not defined

- name: Read the basic-auth user registry
ansible.builtin.slurp:
src: "{{ caddy_users_tsv }}"
register: caddy_tsv_raw
no_log: true
when: not ansible_check_mode or caddy_tsv_stat.stat.exists

- name: Render the basic_auth import file from the registry
ansible.builtin.copy:
Expand All @@ -49,6 +66,8 @@
}
no_log: true
notify: Restart caddy
# caddy_tsv_raw is only defined when the slurp above ran (skipped in check mode).
when: not ansible_check_mode or caddy_tsv_stat.stat.exists

- name: Install the Caddyfile
ansible.builtin.template:
Expand All @@ -59,12 +78,15 @@
mode: "0644"
validate: "caddy validate --adapter caddyfile --config %s"
notify: Restart caddy
# validate shells out to the caddy binary, absent under --check on a fresh host.
when: not ansible_check_mode or 'caddy.service' in (ansible_facts.services | default({}))

- name: Enable and start caddy
ansible.builtin.systemd:
name: caddy
enabled: true
state: started
when: not ansible_check_mode or 'caddy.service' in (ansible_facts.services | default({}))

- name: Apply caddy config changes now
ansible.builtin.meta: flush_handlers
Loading