Implement reproducible S1 handoff contracts
Assistant: codex Assistant-Model: gpt-5.6-sol Assistant-Session: 01a02994-7685-7940-bf34-3555b8256018
This commit is contained in:
parent
c8cb1c8edf
commit
b93af8cc78
44 changed files with 2035 additions and 342 deletions
|
|
@ -5,8 +5,6 @@
|
|||
# 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
|
||||
|
|
|
|||
|
|
@ -1,16 +1,28 @@
|
|||
#!/usr/bin/env python3
|
||||
import json, yaml, subprocess, os, sys, pathlib, glob
|
||||
|
||||
REPO_ROOT = pathlib.Path(__file__).resolve().parents[1]
|
||||
sys.path.insert(0, str(REPO_ROOT / "scripts"))
|
||||
|
||||
from baseline_contract import load_spec, profile_hostvars
|
||||
from inventory_contract import load_inventory
|
||||
|
||||
def load_servers():
|
||||
with open(os.path.join(os.path.dirname(__file__), '..', 'inventory', 'servers.yaml')) as f:
|
||||
data = yaml.safe_load(f)
|
||||
data = load_inventory(REPO_ROOT / 'inventory' / 'servers.yaml')
|
||||
servers = data.get('servers', [])
|
||||
return servers
|
||||
|
||||
def load_baseline():
|
||||
return load_spec(REPO_ROOT / 'spec' / 'server-baseline.yaml')
|
||||
|
||||
def load_tf_outputs():
|
||||
# Try to read terraform outputs to attach IPs, if available.
|
||||
try:
|
||||
out = subprocess.check_output(['terraform', '-chdir=../terraform/hetzner', 'output', '-json'], stderr=subprocess.DEVNULL, text=True)
|
||||
out = subprocess.check_output(
|
||||
['terraform', f'-chdir={REPO_ROOT / "terraform" / "hetzner"}', 'output', '-json'],
|
||||
stderr=subprocess.DEVNULL,
|
||||
text=True,
|
||||
)
|
||||
j = json.loads(out)
|
||||
servers = j.get('servers', {}).get('value', {})
|
||||
return servers # {name: ip}
|
||||
|
|
@ -40,6 +52,7 @@ def load_host_vars(name):
|
|||
|
||||
def main():
|
||||
server_list = load_servers()
|
||||
baseline = load_baseline()
|
||||
tf = load_tf_outputs()
|
||||
host_names = []
|
||||
hostvars = {}
|
||||
|
|
@ -50,6 +63,7 @@ def main():
|
|||
"ansible_host": tf.get(name) or s.get('ip'),
|
||||
"ansible_user": s.get('ssh_user', 'admin'),
|
||||
}
|
||||
hvars.update(profile_hostvars(baseline, s['baseline_profile']))
|
||||
if s.get('ssh_key'):
|
||||
hvars["ansible_ssh_private_key_file"] = s['ssh_key']
|
||||
hvars.update(load_host_vars(name))
|
||||
|
|
|
|||
|
|
@ -2,7 +2,6 @@
|
|||
become: true
|
||||
vars_files:
|
||||
- ../inventory/group_vars/all.yaml
|
||||
- ../inventory/group_vars/secrets.sops.yaml
|
||||
roles:
|
||||
- role: base
|
||||
tags: [base]
|
||||
|
|
|
|||
|
|
@ -1,17 +1,22 @@
|
|||
---
|
||||
- name: Require the executable baseline contract
|
||||
tags: [base, baseline]
|
||||
ansible.builtin.assert:
|
||||
that:
|
||||
- baseline_required_packages is defined
|
||||
- baseline_ssh_directives is defined
|
||||
- baseline_user is defined
|
||||
- baseline_security is defined
|
||||
- baseline_firewall is defined
|
||||
- ufw_manage == baseline_firewall.managed
|
||||
fail_msg: >-
|
||||
Resolve a baseline_profile from spec/server-baseline.yaml through the
|
||||
dynamic inventory before running this role.
|
||||
|
||||
- name: Ensure base packages
|
||||
tags: [base, packages]
|
||||
ansible.builtin.package:
|
||||
name:
|
||||
- apt-transport-https
|
||||
- ca-certificates
|
||||
- curl
|
||||
- git
|
||||
- vim
|
||||
- ufw
|
||||
- fail2ban
|
||||
- python3
|
||||
- python3-venv
|
||||
name: "{{ baseline_required_packages }}"
|
||||
state: present
|
||||
update_cache: true
|
||||
|
||||
|
|
@ -23,11 +28,29 @@
|
|||
group: root
|
||||
mode: '0644'
|
||||
content: |
|
||||
PasswordAuthentication no
|
||||
PermitRootLogin no
|
||||
PubkeyAuthentication yes
|
||||
{% for directive in baseline_ssh_directives | dict2items %}
|
||||
{{ directive.key }} {{ directive.value }}
|
||||
{% endfor %}
|
||||
notify: Restart sshd
|
||||
|
||||
- name: Ensure baseline operator user exists
|
||||
tags: [base, user]
|
||||
ansible.builtin.user:
|
||||
name: "{{ baseline_user.name }}"
|
||||
state: present
|
||||
shell: "{{ baseline_user.shell }}"
|
||||
create_home: true
|
||||
|
||||
- name: Ensure declared passwordless sudo posture
|
||||
tags: [base, user, sudo]
|
||||
ansible.builtin.copy:
|
||||
dest: "/etc/sudoers.d/{{ baseline_user.name }}"
|
||||
owner: root
|
||||
group: root
|
||||
mode: '0440'
|
||||
content: "{{ baseline_user.name }} ALL=(ALL) {{ baseline_user.sudo }}:ALL\n"
|
||||
validate: /usr/sbin/visudo -cf %s
|
||||
|
||||
- name: Ensure .ssh directory exists for ops_bridge_user
|
||||
tags: [base, ssh]
|
||||
ansible.builtin.file:
|
||||
|
|
@ -194,24 +217,25 @@
|
|||
state: started
|
||||
enabled: true
|
||||
|
||||
- name: Configure fail2ban SSH jail
|
||||
- name: Configure declared fail2ban jails
|
||||
tags: [base, fail2ban]
|
||||
ansible.builtin.copy:
|
||||
dest: /etc/fail2ban/jail.d/sshd.conf
|
||||
dest: "/etc/fail2ban/jail.d/{{ item }}.conf"
|
||||
owner: root
|
||||
group: root
|
||||
mode: '0644'
|
||||
content: |
|
||||
[sshd]
|
||||
[{{ item }}]
|
||||
enabled = true
|
||||
port = ssh
|
||||
filter = sshd
|
||||
port = {{ 'ssh' if item == 'sshd' else item }}
|
||||
filter = {{ item }}
|
||||
maxretry = 5
|
||||
bantime = 3600
|
||||
findtime = 600
|
||||
loop: "{{ baseline_security.fail2ban_jails }}"
|
||||
notify: Restart fail2ban
|
||||
|
||||
- name: Set HISTCONTROL to ignorespace
|
||||
- name: Set declared HISTCONTROL
|
||||
tags: [base, histcontrol]
|
||||
ansible.builtin.copy:
|
||||
dest: /etc/profile.d/histcontrol.sh
|
||||
|
|
@ -219,7 +243,7 @@
|
|||
group: root
|
||||
mode: '0644'
|
||||
content: |
|
||||
export HISTCONTROL=ignorespace
|
||||
export HISTCONTROL={{ baseline_security.histcontrol }}
|
||||
|
||||
- name: Set timezone
|
||||
tags: [base, timezone]
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue