Close RAIL-HO-WP-0009 declared-state gaps; leave live 6443 prune gated
Make the k3s API tunnel-only (ADR-005), stop declaring Flannel VXLAN open to Anywhere, tag the base role so firewall can be scoped, and schedule the Goss declared-vs-live check. CoulombCore sets ufw_manage false so a converge cannot enable UFW there. T02 still needs operator approval for make converge-firewall HOST=Railiance01.
This commit is contained in:
parent
434121be99
commit
4d9e77c968
29 changed files with 793 additions and 99 deletions
|
|
@ -26,29 +26,37 @@ ops_bridge_user: tegwick
|
|||
ops_bridge_pubkey: "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIKQmXbAVlEa8dzGx8Hk2S7AITpz6sMWdCN0MeMOzL82u ops-bridge@custodian"
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# k3s API firewall allowlist (6443/tcp)
|
||||
# k3s API firewall (6443/tcp) — tunnel only (ADR-005)
|
||||
#
|
||||
# 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.
|
||||
# Public allowlist is empty. Reach the API over ops-bridge:
|
||||
# bridge up k3s-api-railiance01 # local 16444
|
||||
# bridge up k3s-api-coulombcore # local 16443
|
||||
#
|
||||
# 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"
|
||||
- address: "89.244.90.255"
|
||||
comment: "k3s-api-operator-current"
|
||||
# Revoked addresses are pruned on a firewall-tagged converge so rotated or
|
||||
# retired grants do not remain standing. Do not add new public sources here
|
||||
# without amending ADR-005.
|
||||
k3s_api_allowed_sources: []
|
||||
|
||||
k3s_api_revoked_sources:
|
||||
- address: "89.244.90.248"
|
||||
comment: "hand grant added 2026-08-12/15; retired by ADR-005"
|
||||
- address: "89.244.90.236"
|
||||
comment: "rotated ISP lease; retired by ADR-005"
|
||||
- address: "89.244.90.255"
|
||||
comment: "rotated ISP lease; retired by ADR-005"
|
||||
- address: "89.244.90.246"
|
||||
comment: "rotated ISP lease, superseded 2026-08-11"
|
||||
- address: "85.132.220.102"
|
||||
comment: "historic operator address, retired"
|
||||
comment: "historic operator address; also the 2026-08-15 workstation lease"
|
||||
|
||||
# Single-node clusters need no public VXLAN grant. Set peer addresses here
|
||||
# only when a second node must exchange Flannel frames (RAIL-BS-WP-0007).
|
||||
flannel_vxlan_allowed_sources: []
|
||||
|
||||
# HostEurope Nydus agent — provider dashboard, root-password reset, backups.
|
||||
# Required by the VPS platform (hosteurope/260308-dependency-nydus.md).
|
||||
# Source-restricting it would break the provider; Anywhere is intentional.
|
||||
ufw_extra_allowed:
|
||||
- port: "2224"
|
||||
proto: tcp
|
||||
comment: "nydus-ex-api dashboard agent"
|
||||
|
|
|
|||
|
|
@ -1,6 +1,12 @@
|
|||
# Host-specific variables for CoulombCore (92.205.130.254)
|
||||
# k3s single-node cluster host — HostEurope
|
||||
|
||||
# Do not enable or rewrite UFW on this host. Live filter is iptables INPUT
|
||||
# DROP with a Plesk-era accept list (UFW status: inactive). Enabling UFW
|
||||
# here would take down 80/443 and the rest of the accepted surface unless
|
||||
# every live accept is declared first. RAIL-HO-WP-0009-T03.
|
||||
ufw_manage: false
|
||||
|
||||
# Swapfile (T01)
|
||||
swap_size_gb: 4
|
||||
swap_swappiness: 10
|
||||
|
|
|
|||
|
|
@ -18,12 +18,24 @@ def load_tf_outputs():
|
|||
return {}
|
||||
|
||||
def load_host_vars(name):
|
||||
"""Load host_vars/<name>.yml if it exists."""
|
||||
"""Load host_vars/<name>.yml if it exists.
|
||||
|
||||
The inventory script is ansible/inventory_from_yaml.py. Ansible does not
|
||||
auto-load a host_vars directory next to a script inventory, so this has
|
||||
to emit hostvars itself. Look in ansible/inventory/host_vars first (where
|
||||
CoulombCore.yml actually lives), then the unused repo-root path.
|
||||
"""
|
||||
script_dir = os.path.dirname(__file__)
|
||||
path = os.path.join(script_dir, '..', 'inventory', 'host_vars', f'{name}.yml')
|
||||
if os.path.exists(path):
|
||||
with open(path) as f:
|
||||
return yaml.safe_load(f) or {}
|
||||
candidates = [
|
||||
os.path.join(script_dir, 'inventory', 'host_vars', f'{name}.yml'),
|
||||
os.path.join(script_dir, 'inventory', 'host_vars', f'{name}.yaml'),
|
||||
os.path.join(script_dir, '..', 'inventory', 'host_vars', f'{name}.yml'),
|
||||
os.path.join(script_dir, '..', 'inventory', 'host_vars', f'{name}.yaml'),
|
||||
]
|
||||
for path in candidates:
|
||||
if os.path.exists(path):
|
||||
with open(path) as f:
|
||||
return yaml.safe_load(f) or {}
|
||||
return {}
|
||||
|
||||
def main():
|
||||
|
|
|
|||
|
|
@ -5,8 +5,13 @@
|
|||
- ../inventory/group_vars/secrets.sops.yaml
|
||||
roles:
|
||||
- role: base
|
||||
tags: [base]
|
||||
- role: sops_agent
|
||||
tags: [sops]
|
||||
- role: custodian_agent # injects ~/.ssh/id_custodian_agent.pub into authorized_keys
|
||||
tags: [custodian_agent]
|
||||
- role: swapfile # provisions swap file (size + swappiness from host_vars)
|
||||
tags: [swap]
|
||||
- role: resource_limits # nproc PAM caps + systemd user slice memory limits
|
||||
tags: [resource_limits]
|
||||
# - role: wireguard # enable if you configure WireGuard variables
|
||||
|
|
|
|||
32
ansible/playbooks/goss-status.yaml
Normal file
32
ansible/playbooks/goss-status.yaml
Normal file
|
|
@ -0,0 +1,32 @@
|
|||
---
|
||||
# Fetch the last on-host Goss timer result. Does not run a new check.
|
||||
# Usage: ansible-playbook ansible/playbooks/goss-status.yaml
|
||||
# make goss-status
|
||||
|
||||
- hosts: all
|
||||
become: true
|
||||
gather_facts: false
|
||||
tasks:
|
||||
- name: Read last Goss timer status
|
||||
ansible.builtin.slurp:
|
||||
src: /var/lib/railiance/goss/last.status
|
||||
register: goss_status
|
||||
failed_when: false
|
||||
|
||||
- name: Show last Goss timer status
|
||||
ansible.builtin.debug:
|
||||
msg: >-
|
||||
{{ inventory_hostname }}:
|
||||
{{ goss_status.content | default('') | b64decode | trim
|
||||
if goss_status.content is defined
|
||||
else 'no timer result yet' }}
|
||||
|
||||
- name: Fail when the last on-host check reported FAILED
|
||||
ansible.builtin.stat:
|
||||
path: /var/lib/railiance/goss/FAILED
|
||||
register: goss_failed
|
||||
|
||||
- name: Report failed hosts
|
||||
ansible.builtin.fail:
|
||||
msg: "Goss baseline last run failed on {{ inventory_hostname }}"
|
||||
when: goss_failed.stat.exists | default(false)
|
||||
|
|
@ -1,6 +1,12 @@
|
|||
---
|
||||
# Base role defaults.
|
||||
|
||||
# When false, this role will not enable or rewrite UFW. Use that for hosts
|
||||
# whose live packet filter is not UFW (CoulombCore: iptables INPUT DROP with
|
||||
# a Plesk-era accept list). Enabling UFW there is an availability decision,
|
||||
# not a side effect of an unrelated converge.
|
||||
ufw_manage: true
|
||||
|
||||
# Source addresses permitted to reach the k3s API (6443/tcp).
|
||||
#
|
||||
# WHY THIS EXISTS
|
||||
|
|
@ -10,21 +16,45 @@
|
|||
# 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.
|
||||
# Operator addresses rotate (dynamic ISP leases). An allowlist is a treadmill:
|
||||
# each rotation is either an outage or a stale grant to whoever the ISP
|
||||
# reassigns the address to. RAIL-HO-WP-0009-T04 therefore keeps this list
|
||||
# empty. Reach the API over the ops-bridge SSH tunnel
|
||||
# (`k3s-api-railiance01`, local port 16444; `k3s-api-coulombcore`, 16443).
|
||||
# See docs/adr/ADR-005-k3s-api-tunnel-only.md.
|
||||
#
|
||||
# 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:
|
||||
# Each entry, if any:
|
||||
# - 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.
|
||||
# Move an address here when it stops being yours (or when the public
|
||||
# allowlist is retired); convergence then prunes it.
|
||||
k3s_api_revoked_sources: []
|
||||
|
||||
# Source addresses permitted to send Flannel VXLAN (8472/udp).
|
||||
#
|
||||
# Empty by default. A single-node cluster does not need a public VXLAN
|
||||
# grant; adding an unrestricted 8472/udp allow would expose the pod network
|
||||
# to injection. Set this to the other nodes' addresses only when the cluster
|
||||
# becomes multi-node (RAIL-BS-WP-0007 / ThreePhoenix HA).
|
||||
#
|
||||
# Each entry:
|
||||
# - address: "203.0.113.20"
|
||||
# comment: "flannel-vxlan-peer"
|
||||
flannel_vxlan_allowed_sources: []
|
||||
|
||||
# Extra UFW allows that are not k3s. Used for provider agents that must stay
|
||||
# reachable (HostEurope Nydus on 2224/tcp). Empty by default so a Hetzner
|
||||
# host does not inherit a HostEurope-only hole.
|
||||
#
|
||||
# Each entry:
|
||||
# - port: "2224"
|
||||
# proto: tcp
|
||||
# comment: "nydus-ex-api dashboard agent"
|
||||
ufw_extra_allowed: []
|
||||
|
|
|
|||
|
|
@ -1,4 +1,9 @@
|
|||
---
|
||||
- name: Restart sshd
|
||||
ansible.builtin.service:
|
||||
name: ssh
|
||||
state: restarted
|
||||
|
||||
- name: Restart fail2ban
|
||||
ansible.builtin.service:
|
||||
name: fail2ban
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
---
|
||||
- name: Ensure base packages
|
||||
tags: [base, packages]
|
||||
ansible.builtin.package:
|
||||
name:
|
||||
- apt-transport-https
|
||||
|
|
@ -15,6 +16,7 @@
|
|||
update_cache: true
|
||||
|
||||
- name: Harden SSH
|
||||
tags: [base, ssh]
|
||||
ansible.builtin.copy:
|
||||
dest: /etc/ssh/sshd_config.d/10-hardening.conf
|
||||
owner: root
|
||||
|
|
@ -24,13 +26,10 @@
|
|||
PasswordAuthentication no
|
||||
PermitRootLogin no
|
||||
PubkeyAuthentication yes
|
||||
|
||||
- name: Restart sshd
|
||||
ansible.builtin.service:
|
||||
name: ssh
|
||||
state: restarted
|
||||
notify: Restart sshd
|
||||
|
||||
- name: Ensure .ssh directory exists for ops_bridge_user
|
||||
tags: [base, ssh]
|
||||
ansible.builtin.file:
|
||||
path: "/home/{{ ops_bridge_user | default('tegwick') }}/.ssh"
|
||||
state: directory
|
||||
|
|
@ -39,6 +38,7 @@
|
|||
mode: '0700'
|
||||
|
||||
- name: Inject ops-bridge public key into authorized_keys
|
||||
tags: [base, ssh]
|
||||
ansible.posix.authorized_key:
|
||||
user: "{{ ops_bridge_user | default('tegwick') }}"
|
||||
key: "{{ ops_bridge_pubkey }}"
|
||||
|
|
@ -47,27 +47,46 @@
|
|||
when: ops_bridge_pubkey is defined and ops_bridge_pubkey | length > 0
|
||||
|
||||
- name: Configure UFW default incoming policy
|
||||
tags: [base, firewall, ufw]
|
||||
ansible.builtin.ufw:
|
||||
state: enabled
|
||||
policy: deny
|
||||
direction: incoming
|
||||
when: ufw_manage | bool
|
||||
|
||||
- name: Allow UFW routing (required for k3s flannel pod networking)
|
||||
- name: Allow UFW routing when VXLAN peers are declared
|
||||
tags: [base, firewall, ufw]
|
||||
ansible.builtin.ufw:
|
||||
policy: allow
|
||||
direction: routed
|
||||
when: ufw_manage | bool and (flannel_vxlan_allowed_sources | length > 0)
|
||||
|
||||
- name: Allow SSH in UFW
|
||||
tags: [base, firewall, ufw]
|
||||
ansible.builtin.ufw:
|
||||
rule: allow
|
||||
name: OpenSSH
|
||||
when: ufw_manage | bool
|
||||
|
||||
# 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 declared extra UFW ports
|
||||
tags: [base, firewall, ufw]
|
||||
ansible.builtin.ufw:
|
||||
rule: allow
|
||||
port: "{{ item.port }}"
|
||||
proto: "{{ item.proto | default('tcp') }}"
|
||||
comment: "{{ item.comment | default('extra-allow') }}"
|
||||
loop: "{{ ufw_extra_allowed }}"
|
||||
loop_control:
|
||||
label: "{{ item.port }}/{{ item.proto | default('tcp') }}"
|
||||
when: ufw_manage | bool
|
||||
|
||||
# k3s API access is source-restricted and empty by default (tunnel-only).
|
||||
# See roles/base/defaults/main.yml and docs/adr/ADR-005-k3s-api-tunnel-only.md.
|
||||
# Order matters: remaining grants (if any) are added BEFORE the blanket rule
|
||||
# is removed, so a non-empty allowlist never opens a window without API access.
|
||||
|
||||
- name: Allow k3s API from approved operator sources only
|
||||
tags: [base, firewall, ufw]
|
||||
ansible.builtin.ufw:
|
||||
rule: allow
|
||||
port: '6443'
|
||||
|
|
@ -77,15 +96,19 @@
|
|||
loop: "{{ k3s_api_allowed_sources }}"
|
||||
loop_control:
|
||||
label: "{{ item.address }}"
|
||||
when: ufw_manage | bool
|
||||
|
||||
- name: Remove blanket k3s API rule if present (must not be world-reachable)
|
||||
tags: [base, firewall, ufw]
|
||||
ansible.builtin.ufw:
|
||||
rule: allow
|
||||
port: '6443'
|
||||
proto: tcp
|
||||
delete: true
|
||||
when: ufw_manage | bool
|
||||
|
||||
- name: Revoke k3s API access for retired operator sources
|
||||
tags: [base, firewall, ufw]
|
||||
ansible.builtin.ufw:
|
||||
rule: allow
|
||||
port: '6443'
|
||||
|
|
@ -95,29 +118,49 @@
|
|||
loop: "{{ k3s_api_revoked_sources }}"
|
||||
loop_control:
|
||||
label: "{{ item.address }}"
|
||||
when: ufw_manage | bool
|
||||
|
||||
- name: Warn when no operator source is allowed to reach the k3s API
|
||||
tags: [base, firewall, ufw]
|
||||
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
|
||||
sources on this host. Reach the API over the ops-bridge tunnel
|
||||
(k3s-api-railiance01 on 16444, k3s-api-coulombcore on 16443). SSH is
|
||||
unaffected and the host remains recoverable.
|
||||
when: ufw_manage | bool and (k3s_api_allowed_sources | length == 0)
|
||||
|
||||
- name: Allow Flannel VXLAN in UFW
|
||||
- name: Allow Flannel VXLAN from declared cluster peers only
|
||||
tags: [base, firewall, ufw]
|
||||
ansible.builtin.ufw:
|
||||
rule: allow
|
||||
port: '8472'
|
||||
proto: udp
|
||||
from_ip: "{{ item.address }}"
|
||||
comment: "{{ item.comment | default('flannel-vxlan-peer') }}"
|
||||
loop: "{{ flannel_vxlan_allowed_sources }}"
|
||||
loop_control:
|
||||
label: "{{ item.address }}"
|
||||
when: ufw_manage | bool
|
||||
|
||||
- name: Remove blanket Flannel VXLAN rule if present (must not be world-reachable)
|
||||
tags: [base, firewall, ufw]
|
||||
ansible.builtin.ufw:
|
||||
rule: allow
|
||||
port: '8472'
|
||||
proto: udp
|
||||
delete: true
|
||||
when: ufw_manage | bool
|
||||
|
||||
- name: Enable fail2ban
|
||||
tags: [base, fail2ban]
|
||||
ansible.builtin.service:
|
||||
name: fail2ban
|
||||
state: started
|
||||
enabled: true
|
||||
|
||||
- name: Configure fail2ban SSH jail
|
||||
tags: [base, fail2ban]
|
||||
ansible.builtin.copy:
|
||||
dest: /etc/fail2ban/jail.d/sshd.conf
|
||||
owner: root
|
||||
|
|
@ -134,6 +177,7 @@
|
|||
notify: Restart fail2ban
|
||||
|
||||
- name: Set HISTCONTROL to ignorespace
|
||||
tags: [base, histcontrol]
|
||||
ansible.builtin.copy:
|
||||
dest: /etc/profile.d/histcontrol.sh
|
||||
owner: root
|
||||
|
|
@ -143,5 +187,6 @@
|
|||
export HISTCONTROL=ignorespace
|
||||
|
||||
- name: Set timezone
|
||||
tags: [base, timezone]
|
||||
community.general.timezone:
|
||||
name: "{{ timezone | default('UTC') }}"
|
||||
|
|
|
|||
59
ansible/roles/goss/files/goss-baseline-check.sh
Executable file
59
ansible/roles/goss/files/goss-baseline-check.sh
Executable file
|
|
@ -0,0 +1,59 @@
|
|||
#!/bin/bash
|
||||
# Recurring declared-vs-live check. Installed by the goss role.
|
||||
# Writes TAP + a one-line status; notifies only on pass/fail transitions.
|
||||
set -euo pipefail
|
||||
|
||||
GOSS_BIN="${GOSS_BIN:-/usr/local/bin/goss}"
|
||||
GOSS_FILE="${GOSS_FILE:-/etc/goss/baseline.yaml}"
|
||||
STATE_DIR="${STATE_DIR:-/var/lib/railiance/goss}"
|
||||
NOTIFY_URL="${RAILIANCE_GOSS_NOTIFY_URL:-}"
|
||||
HOST="$(hostname -s)"
|
||||
|
||||
mkdir -p "${STATE_DIR}"
|
||||
chmod 0755 "${STATE_DIR}"
|
||||
|
||||
if [[ ! -x "${GOSS_BIN}" || ! -f "${GOSS_FILE}" ]]; then
|
||||
echo "skip: goss binary or baseline missing" | tee "${STATE_DIR}/last.status"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
set +e
|
||||
"${GOSS_BIN}" -g "${GOSS_FILE}" validate --format tap > "${STATE_DIR}/last.tap"
|
||||
rc=$?
|
||||
set -e
|
||||
|
||||
if [[ "${rc}" -eq 0 ]]; then
|
||||
result=pass
|
||||
else
|
||||
result=fail
|
||||
fi
|
||||
|
||||
printf 'result=%s host=%s ts=%s rc=%s\n' \
|
||||
"${result}" "${HOST}" "$(date -u +%Y-%m-%dT%H:%M:%SZ)" "${rc}" \
|
||||
> "${STATE_DIR}/last.status"
|
||||
|
||||
prev=""
|
||||
if [[ -f "${STATE_DIR}/last.result" ]]; then
|
||||
prev="$(cat "${STATE_DIR}/last.result")"
|
||||
fi
|
||||
echo "${result}" > "${STATE_DIR}/last.result"
|
||||
|
||||
logger -t railiance-goss "baseline ${result} on ${HOST} (rc=${rc})"
|
||||
|
||||
if [[ "${result}" == "fail" ]]; then
|
||||
touch "${STATE_DIR}/FAILED"
|
||||
else
|
||||
rm -f "${STATE_DIR}/FAILED"
|
||||
fi
|
||||
|
||||
if [[ -n "${NOTIFY_URL}" && "${result}" != "${prev}" ]]; then
|
||||
payload=$(printf \
|
||||
'{"summary":"Goss baseline %s on %s","event_type":"note","author":"railiance-goss-timer"}' \
|
||||
"${result}" "${HOST}")
|
||||
curl -sS -m 10 -X POST "${NOTIFY_URL}" \
|
||||
-H 'Content-Type: application/json' \
|
||||
-d "${payload}" >/dev/null || \
|
||||
logger -t railiance-goss "notify failed for ${HOST} ${result}"
|
||||
fi
|
||||
|
||||
exit "${rc}"
|
||||
4
ansible/roles/goss/handlers/main.yml
Normal file
4
ansible/roles/goss/handlers/main.yml
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
---
|
||||
- name: Reload systemd daemon
|
||||
ansible.builtin.systemd:
|
||||
daemon_reload: true
|
||||
|
|
@ -32,6 +32,39 @@
|
|||
group: root
|
||||
mode: "0644"
|
||||
|
||||
- name: Install recurring Goss check wrapper
|
||||
ansible.builtin.copy:
|
||||
src: goss-baseline-check.sh
|
||||
dest: /usr/local/sbin/goss-baseline-check
|
||||
owner: root
|
||||
group: root
|
||||
mode: "0755"
|
||||
|
||||
- name: Install Goss baseline systemd service
|
||||
ansible.builtin.template:
|
||||
src: railiance-goss-baseline.service.j2
|
||||
dest: /etc/systemd/system/railiance-goss-baseline.service
|
||||
owner: root
|
||||
group: root
|
||||
mode: "0644"
|
||||
notify: Reload systemd daemon
|
||||
|
||||
- name: Install Goss baseline systemd timer
|
||||
ansible.builtin.template:
|
||||
src: railiance-goss-baseline.timer.j2
|
||||
dest: /etc/systemd/system/railiance-goss-baseline.timer
|
||||
owner: root
|
||||
group: root
|
||||
mode: "0644"
|
||||
notify: Reload systemd daemon
|
||||
|
||||
- name: Enable hourly Goss baseline timer
|
||||
ansible.builtin.systemd:
|
||||
name: railiance-goss-baseline.timer
|
||||
enabled: true
|
||||
state: started
|
||||
daemon_reload: true
|
||||
|
||||
- name: Run Goss assertions (TAP output)
|
||||
ansible.builtin.command:
|
||||
cmd: "{{ goss_bin }} -g {{ goss_dir }}/baseline.yaml validate --format tap"
|
||||
|
|
|
|||
|
|
@ -0,0 +1,10 @@
|
|||
[Unit]
|
||||
Description=Railiance declared-vs-live Goss baseline
|
||||
Documentation=file:///etc/goss/baseline.yaml
|
||||
|
||||
[Service]
|
||||
Type=oneshot
|
||||
ExecStart=/usr/local/sbin/goss-baseline-check
|
||||
Nice=10
|
||||
# The wrapper records TAP even when assertions fail.
|
||||
SuccessExitStatus=0 1
|
||||
|
|
@ -0,0 +1,12 @@
|
|||
[Unit]
|
||||
Description=Hourly Railiance Goss baseline (RAIL-HO-WP-0009-T05)
|
||||
|
||||
[Timer]
|
||||
OnBootSec=5min
|
||||
OnUnitActiveSec=1h
|
||||
RandomizedDelaySec=5min
|
||||
Persistent=true
|
||||
Unit=railiance-goss-baseline.service
|
||||
|
||||
[Install]
|
||||
WantedBy=timers.target
|
||||
|
|
@ -10,6 +10,7 @@
|
|||
# user_memory_swap_max: systemd MemorySwapMax (default: 512M)
|
||||
|
||||
- name: Set PAM nproc limits
|
||||
tags: [resource_limits]
|
||||
ansible.builtin.template:
|
||||
src: nproc-limits.conf.j2
|
||||
dest: /etc/security/limits.d/60-nproc-{{ resource_limit_user | default('tegwick') }}.conf
|
||||
|
|
@ -18,6 +19,7 @@
|
|||
mode: '0644'
|
||||
|
||||
- name: Ensure systemd user slice override directory
|
||||
tags: [resource_limits]
|
||||
ansible.builtin.file:
|
||||
path: "/etc/systemd/system/user-{{ resource_limit_uid | default(1000) }}.slice.d"
|
||||
state: directory
|
||||
|
|
@ -26,6 +28,7 @@
|
|||
mode: '0755'
|
||||
|
||||
- name: Set systemd user slice memory limits
|
||||
tags: [resource_limits]
|
||||
ansible.builtin.template:
|
||||
src: user-slice-limits.conf.j2
|
||||
dest: "/etc/systemd/system/user-{{ resource_limit_uid | default(1000) }}.slice.d/limits.conf"
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
---
|
||||
- name: Install age
|
||||
tags: [sops]
|
||||
ansible.builtin.shell: |
|
||||
set -euo pipefail
|
||||
if ! command -v age >/dev/null; then
|
||||
|
|
@ -9,12 +10,14 @@
|
|||
executable: /bin/bash
|
||||
|
||||
- name: Install sops
|
||||
tags: [sops]
|
||||
ansible.builtin.get_url:
|
||||
url: https://github.com/getsops/sops/releases/download/v3.9.0/sops-v3.9.0.linux.amd64
|
||||
dest: /usr/local/bin/sops
|
||||
mode: '0755'
|
||||
|
||||
- name: Create SOPS age dir
|
||||
tags: [sops]
|
||||
ansible.builtin.file:
|
||||
path: /root/.config/sops/age
|
||||
state: directory
|
||||
|
|
|
|||
|
|
@ -6,17 +6,20 @@
|
|||
# swap_swappiness: vm.swappiness value (default: 10)
|
||||
|
||||
- name: Check if swapfile exists with correct size
|
||||
tags: [swap]
|
||||
ansible.builtin.stat:
|
||||
path: /swapfile
|
||||
register: swapfile_stat
|
||||
|
||||
- name: Allocate swapfile (fallocate)
|
||||
tags: [swap]
|
||||
ansible.builtin.command:
|
||||
cmd: "fallocate -l {{ (swap_size_gb | default(4)) | int }}G /swapfile"
|
||||
creates: /swapfile
|
||||
when: not swapfile_stat.stat.exists
|
||||
|
||||
- name: Set swapfile permissions
|
||||
tags: [swap]
|
||||
ansible.builtin.file:
|
||||
path: /swapfile
|
||||
owner: root
|
||||
|
|
@ -24,17 +27,20 @@
|
|||
mode: '0600'
|
||||
|
||||
- name: Format swapfile
|
||||
tags: [swap]
|
||||
ansible.builtin.command:
|
||||
cmd: mkswap /swapfile
|
||||
when: not swapfile_stat.stat.exists
|
||||
|
||||
- name: Enable swapfile
|
||||
tags: [swap]
|
||||
ansible.builtin.command:
|
||||
cmd: swapon /swapfile
|
||||
when: not swapfile_stat.stat.exists
|
||||
ignore_errors: true # already active is not an error
|
||||
|
||||
- name: Ensure swapfile in /etc/fstab
|
||||
tags: [swap]
|
||||
ansible.builtin.lineinfile:
|
||||
path: /etc/fstab
|
||||
regexp: '^/swapfile'
|
||||
|
|
@ -42,6 +48,7 @@
|
|||
state: present
|
||||
|
||||
- name: Set vm.swappiness at runtime
|
||||
tags: [swap]
|
||||
ansible.posix.sysctl:
|
||||
name: vm.swappiness
|
||||
value: "{{ swap_swappiness | default(10) }}"
|
||||
|
|
@ -49,6 +56,7 @@
|
|||
reload: true
|
||||
|
||||
- name: Persist vm.swappiness across reboots
|
||||
tags: [swap]
|
||||
ansible.builtin.copy:
|
||||
dest: /etc/sysctl.d/60-swappiness.conf
|
||||
owner: root
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue