Archived
Initial infrastructure mono-repo scaffold
Consolidates nixos, docker, raspi, and debian-configuration into a single infrastructure-as-code repo. Includes: - ansible/: full inventory + proxmox-hardening, freeipa, and raspberrypi roles (converted from debian-configuration bash scripts) - terraform/: Proxmox VMs, Dynu DNS, Pi-hole (decommissioned stub), Docker container catalog — migrated from docker/infrastructure/terraform/ - stacks/docker/, stacks/raspi/, nixos/: placeholder READMEs pending git subtree population (see implementation plan) - docs/: internal MkDocs site with architecture, network topology, runbooks, and drift-detection guide; external sanitized site - scripts/: drift-detect.sh, docs-build.sh, install-hooks.sh, check-secrets.sh - CI: secret-scan (push/PR), drift-detect (daily), docs-build (on change) - Pi-hole removed throughout — DNS is FreeIPA, DHCP is router See docs/internal/implementation-plan.md for the phased rollout after pushing to Gitea. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01UvNjoxTWEDkhXsd1Dq2ETP
This commit is contained in:
@@ -0,0 +1,18 @@
|
||||
[defaults]
|
||||
inventory = ./inventory/hosts.yml
|
||||
collections_path = ./collections
|
||||
retry_files_enabled = False
|
||||
stdout_callback = yaml
|
||||
host_key_checking = True
|
||||
|
||||
# Fail fast on unreachable hosts rather than silently skipping
|
||||
any_errors_fatal = False
|
||||
|
||||
# Use pipelining for speed (requires requiretty disabled in sudoers, which our roles handle)
|
||||
pipelining = True
|
||||
|
||||
[inventory]
|
||||
enable_plugins = yaml, ini
|
||||
|
||||
[ssh_connection]
|
||||
ssh_args = -o ControlMaster=auto -o ControlPersist=60s -o ServerAliveInterval=30
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
collections:
|
||||
- name: ansible.posix
|
||||
- name: community.general
|
||||
- name: ansible.utils
|
||||
@@ -0,0 +1,20 @@
|
||||
---
|
||||
# Variables applied to every host.
|
||||
# Override per-group in group_vars/<group>.yml or per-host in host_vars/<host>/vars.yml.
|
||||
|
||||
ansible_python_interpreter: /usr/bin/python3
|
||||
|
||||
# LAN domain
|
||||
lan_domain: sweet.home
|
||||
tailnet_domain: tail13f623.ts.net
|
||||
|
||||
# DNS: FreeIPA is the authoritative resolver for sweet.home (Pi-hole decommissioned).
|
||||
# All LAN clients point directly to domain-controller.sweet.home for DNS.
|
||||
ipa_realm: SWEET.HOME
|
||||
ipa_server: domain-controller.sweet.home
|
||||
|
||||
# Docker access GID — must match FreeIPA docker-access group GID
|
||||
docker_access_gid: 50010
|
||||
|
||||
# IPA admins group granted passwordless sudo on all enrolled hosts
|
||||
ipa_admin_group: admins
|
||||
@@ -0,0 +1,14 @@
|
||||
---
|
||||
# Proxmox-group defaults.
|
||||
# Per-host overrides go in host_vars/pve1.sweet.home/vars.yml etc.
|
||||
|
||||
# proxmox-hardening role toggles
|
||||
proxmox_harden_ssh: true
|
||||
proxmox_configure_firewall: true
|
||||
proxmox_configure_unattended_upgrades: true
|
||||
proxmox_switch_to_nosub_repo: true
|
||||
proxmox_disable_nag: true
|
||||
proxmox_setup_ipa_sudo: true
|
||||
|
||||
# Firewall: management CIDR — override per host if subnets differ
|
||||
# proxmox_mgmt_cidr is set per host in hosts.yml
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
# Raspberry Pi group defaults.
|
||||
|
||||
raspberrypi_setup_ipa_sudo: true
|
||||
raspberrypi_pin_docker_gid: true
|
||||
@@ -0,0 +1,79 @@
|
||||
---
|
||||
# Full infrastructure inventory.
|
||||
# IPs are documented here for reference; use FQDNs where DNS is reliable.
|
||||
# Hosts marked [nixos] are managed by the NixOS flake (nixos/) and are present
|
||||
# here only for Ansible tasks that apply to them (e.g. drift-check pings).
|
||||
|
||||
all:
|
||||
children:
|
||||
|
||||
# ── Proxmox hypervisors ───────────────────────────────────────────────────
|
||||
proxmox:
|
||||
hosts:
|
||||
pve1.sweet.home:
|
||||
ansible_user: wayne
|
||||
proxmox_node_name: pve
|
||||
proxmox_role: production
|
||||
proxmox_mgmt_cidr: "192.168.2.0/24"
|
||||
proxmox_admin_username: wayne
|
||||
pve-test.sweet.home:
|
||||
ansible_user: wayne
|
||||
proxmox_node_name: pve-test
|
||||
proxmox_role: sandbox
|
||||
proxmox_mgmt_cidr: "192.168.2.0/24"
|
||||
proxmox_admin_username: wayne
|
||||
|
||||
# ── Identity / DNS ────────────────────────────────────────────────────────
|
||||
freeipa:
|
||||
hosts:
|
||||
domain-controller.sweet.home:
|
||||
ansible_user: wayne
|
||||
# IPA server parameters (consumed by freeipa role)
|
||||
ipa_realm: "SWEET.HOME"
|
||||
ipa_domain: "sweet.home"
|
||||
ipa_hostname: "domain-controller.sweet.home"
|
||||
ipa_ip: "192.168.2.253"
|
||||
ipa_dns_forwarder: "192.168.2.138" # Pi-hole
|
||||
ansible_python_interpreter: /usr/bin/python3
|
||||
|
||||
# ── Edge / monitoring ─────────────────────────────────────────────────────
|
||||
raspi:
|
||||
hosts:
|
||||
raspberrypi.tail13f623.ts.net:
|
||||
ansible_user: wayne
|
||||
docker_access_gid: 50010
|
||||
|
||||
# ── NixOS hosts (flake-managed; present for ping/audit tasks only) ────────
|
||||
nixos:
|
||||
vars:
|
||||
ansible_note: >
|
||||
These hosts are managed by the NixOS flake in nixos/.
|
||||
Only non-NixOS tasks (connectivity checks, IPA enrollment helpers)
|
||||
should target this group directly from Ansible.
|
||||
hosts:
|
||||
docker.sweet.home:
|
||||
ansible_host: 192.168.2.225
|
||||
ansible_user: wayne
|
||||
nixos_build_type: docker
|
||||
nix-cache.sweet.home:
|
||||
ansible_host: 192.168.2.224
|
||||
ansible_user: wayne
|
||||
nixos_build_type: nix-cache
|
||||
|
||||
# ── Groupings for playbook targeting ─────────────────────────────────────
|
||||
linux:
|
||||
children:
|
||||
proxmox: {}
|
||||
freeipa: {}
|
||||
raspi: {}
|
||||
nixos: {}
|
||||
|
||||
network:
|
||||
children:
|
||||
freeipa: {}
|
||||
|
||||
non_nixos:
|
||||
children:
|
||||
proxmox: {}
|
||||
freeipa: {}
|
||||
raspi: {}
|
||||
@@ -0,0 +1,18 @@
|
||||
---
|
||||
- name: FreeIPA server provisioning
|
||||
hosts: freeipa
|
||||
become: true
|
||||
gather_facts: true
|
||||
|
||||
pre_tasks:
|
||||
- name: Confirm Rocky Linux 9
|
||||
ansible.builtin.assert:
|
||||
that:
|
||||
- ansible_distribution == "Rocky"
|
||||
- ansible_distribution_major_version == "9"
|
||||
fail_msg: "FreeIPA role targets Rocky Linux 9 only."
|
||||
tags: always
|
||||
|
||||
roles:
|
||||
- role: freeipa
|
||||
tags: freeipa
|
||||
@@ -0,0 +1,8 @@
|
||||
---
|
||||
# Connectivity check — verify all hosts are reachable before a real run.
|
||||
- name: Ping all hosts
|
||||
hosts: all
|
||||
gather_facts: false
|
||||
tasks:
|
||||
- name: Ping
|
||||
ansible.builtin.ping:
|
||||
@@ -0,0 +1,18 @@
|
||||
---
|
||||
- name: Proxmox VE hardening
|
||||
hosts: proxmox
|
||||
become: true
|
||||
gather_facts: true
|
||||
|
||||
pre_tasks:
|
||||
- name: Confirm this is a Proxmox VE host
|
||||
ansible.builtin.assert:
|
||||
that:
|
||||
- ansible_os_family == "Debian"
|
||||
- ansible_facts.packages is not defined or true
|
||||
fail_msg: "This playbook targets Proxmox VE (Debian-based) hosts only."
|
||||
tags: always
|
||||
|
||||
roles:
|
||||
- role: proxmox-hardening
|
||||
tags: proxmox
|
||||
@@ -0,0 +1,22 @@
|
||||
---
|
||||
- name: Raspberry Pi setup
|
||||
hosts: raspi
|
||||
become: true
|
||||
gather_facts: true
|
||||
|
||||
pre_tasks:
|
||||
- name: Confirm IPA enrollment
|
||||
ansible.builtin.stat:
|
||||
path: /etc/ipa/default.conf
|
||||
register: ipa_conf
|
||||
tags: always
|
||||
|
||||
- name: Warn if not IPA-enrolled
|
||||
ansible.builtin.debug:
|
||||
msg: "WARNING: /etc/ipa/default.conf not found. IPA-dependent tasks will be skipped."
|
||||
when: not ipa_conf.stat.exists
|
||||
tags: always
|
||||
|
||||
roles:
|
||||
- role: raspberrypi
|
||||
tags: raspi
|
||||
@@ -0,0 +1,13 @@
|
||||
---
|
||||
# Master playbook — runs every role against its target group.
|
||||
# Always use --check --diff on first run against production hosts.
|
||||
# Use --limit <host_pattern> to target a subset.
|
||||
#
|
||||
# Examples:
|
||||
# ansible-playbook site.yml --check --diff # dry-run everything
|
||||
# ansible-playbook site.yml --limit pve1.sweet.home # production proxmox only
|
||||
# ansible-playbook site.yml --limit proxmox --tags ssh # only SSH tasks on all PVE nodes
|
||||
|
||||
- import_playbook: proxmox.yml
|
||||
- import_playbook: freeipa.yml
|
||||
- import_playbook: raspi.yml
|
||||
@@ -0,0 +1,14 @@
|
||||
---
|
||||
# freeipa role defaults — all overridable in inventory host_vars or group_vars.
|
||||
# Most values come from the per-host inventory (see hosts.yml).
|
||||
|
||||
ipa_realm: "SWEET.HOME"
|
||||
ipa_domain: "sweet.home"
|
||||
|
||||
# Set these per-host in inventory/hosts.yml:
|
||||
# ipa_hostname: "domain-controller.sweet.home"
|
||||
# ipa_ip: "192.168.2.253"
|
||||
# ipa_dns_forwarder: "192.168.2.138"
|
||||
|
||||
# Swap file created if no swap exists (FreeIPA needs headroom during install)
|
||||
ipa_swap_size_mb: 2048
|
||||
@@ -0,0 +1,133 @@
|
||||
---
|
||||
# freeipa/tasks/main.yml
|
||||
# Provisions a FreeIPA 4.x server on Rocky Linux 9.
|
||||
# The ipa-server-install step is intentionally NOT idempotent — it will
|
||||
# refuse to run if IPA is already installed, which acts as a safety guard.
|
||||
|
||||
# ── Pre-flight ────────────────────────────────────────────────────────────────
|
||||
|
||||
- name: Check IPA is not already installed
|
||||
ansible.builtin.stat:
|
||||
path: /etc/ipa/default.conf
|
||||
register: ipa_installed
|
||||
tags: always
|
||||
|
||||
- name: Skip install tasks if IPA already exists
|
||||
ansible.builtin.debug:
|
||||
msg: "FreeIPA already installed — skipping install tasks. Use verify tasks to check health."
|
||||
when: ipa_installed.stat.exists
|
||||
tags: install
|
||||
|
||||
- name: Assert hostname is correct FQDN
|
||||
ansible.builtin.command:
|
||||
cmd: hostname -f
|
||||
register: fqdn_check
|
||||
changed_when: false
|
||||
failed_when: fqdn_check.stdout != ipa_hostname
|
||||
when:
|
||||
- not ipa_installed.stat.exists
|
||||
- ipa_hostname is defined
|
||||
tags: install, preflight
|
||||
|
||||
- name: Assert /etc/hosts has correct entry
|
||||
ansible.builtin.lineinfile:
|
||||
path: /etc/hosts
|
||||
line: "{{ ipa_ip }} {{ ipa_hostname }} {{ ipa_hostname.split('.')[0] }}"
|
||||
state: present
|
||||
when:
|
||||
- not ipa_installed.stat.exists
|
||||
- ipa_ip is defined
|
||||
- ipa_hostname is defined
|
||||
tags: install, preflight
|
||||
|
||||
# ── Swap ──────────────────────────────────────────────────────────────────────
|
||||
|
||||
- name: Check current swap
|
||||
ansible.builtin.command:
|
||||
cmd: swapon --show
|
||||
register: swap_check
|
||||
changed_when: false
|
||||
tags: install, swap
|
||||
|
||||
- name: Create swap file if absent
|
||||
block:
|
||||
- name: Create swap file
|
||||
ansible.builtin.command:
|
||||
cmd: "dd if=/dev/zero of=/swapfile bs=1M count={{ ipa_swap_size_mb }} status=none"
|
||||
args:
|
||||
creates: /swapfile
|
||||
|
||||
- name: Set swap permissions
|
||||
ansible.builtin.file:
|
||||
path: /swapfile
|
||||
mode: "0600"
|
||||
|
||||
- name: Format swap
|
||||
ansible.builtin.command:
|
||||
cmd: mkswap /swapfile
|
||||
changed_when: true
|
||||
|
||||
- name: Enable swap
|
||||
ansible.builtin.command:
|
||||
cmd: swapon /swapfile
|
||||
changed_when: true
|
||||
|
||||
- name: Add swap to fstab
|
||||
ansible.builtin.lineinfile:
|
||||
path: /etc/fstab
|
||||
line: "/swapfile none swap defaults 0 0"
|
||||
state: present
|
||||
when:
|
||||
- not ipa_installed.stat.exists
|
||||
- swap_check.stdout == ""
|
||||
tags: install, swap
|
||||
|
||||
# ── Package install ───────────────────────────────────────────────────────────
|
||||
|
||||
- name: Install FreeIPA server packages
|
||||
ansible.builtin.dnf:
|
||||
name:
|
||||
- freeipa-server
|
||||
- freeipa-server-dns
|
||||
state: present
|
||||
when: not ipa_installed.stat.exists
|
||||
tags: install, packages
|
||||
|
||||
# ── IPA server install ────────────────────────────────────────────────────────
|
||||
|
||||
- name: Run ipa-server-install (interactive passwords via env vars)
|
||||
ansible.builtin.shell:
|
||||
cmd: >
|
||||
ipa-server-install
|
||||
--realm={{ ipa_realm }}
|
||||
--domain={{ ipa_domain }}
|
||||
--hostname={{ ipa_hostname }}
|
||||
--ip-address={{ ipa_ip }}
|
||||
--forwarder={{ ipa_dns_forwarder }}
|
||||
--setup-dns
|
||||
--auto-reverse
|
||||
--no-host-dns
|
||||
--mkhomedir
|
||||
--unattended
|
||||
--ds-password="${IPA_DM_PASSWORD}"
|
||||
--admin-password="${IPA_ADMIN_PASSWORD}"
|
||||
environment:
|
||||
IPA_DM_PASSWORD: "{{ ipa_dm_password }}"
|
||||
IPA_ADMIN_PASSWORD: "{{ ipa_admin_password }}"
|
||||
no_log: true
|
||||
when: not ipa_installed.stat.exists
|
||||
tags: install
|
||||
|
||||
# ── Verification ──────────────────────────────────────────────────────────────
|
||||
|
||||
- name: Verify IPA services are running
|
||||
ansible.builtin.command:
|
||||
cmd: ipactl status
|
||||
register: ipa_status
|
||||
changed_when: false
|
||||
tags: verify
|
||||
|
||||
- name: Print IPA service status
|
||||
ansible.builtin.debug:
|
||||
var: ipa_status.stdout_lines
|
||||
tags: verify
|
||||
@@ -0,0 +1,22 @@
|
||||
---
|
||||
# proxmox-hardening role defaults.
|
||||
# Override in inventory/group_vars/proxmox.yml or host_vars/<host>/vars.yml.
|
||||
|
||||
proxmox_harden_ssh: true
|
||||
proxmox_configure_firewall: true
|
||||
proxmox_configure_unattended_upgrades: true
|
||||
proxmox_switch_to_nosub_repo: true
|
||||
proxmox_disable_nag: true
|
||||
proxmox_setup_ipa_sudo: true
|
||||
|
||||
# Management CIDR for the PVE datacenter firewall (inbound allow-list).
|
||||
# Must be set per-host in inventory (hosts.yml) — no default here to force explicit assignment.
|
||||
# proxmox_mgmt_cidr: "192.168.2.0/24"
|
||||
|
||||
# Named PVE admin username to create (set to "" to skip user creation).
|
||||
# proxmox_admin_username: "wayne"
|
||||
|
||||
# fail2ban sshd jail settings
|
||||
proxmox_fail2ban_maxretry: 5
|
||||
proxmox_fail2ban_bantime: "1h"
|
||||
proxmox_fail2ban_findtime: "10m"
|
||||
@@ -0,0 +1,19 @@
|
||||
---
|
||||
- name: apt update
|
||||
ansible.builtin.apt:
|
||||
update_cache: true
|
||||
|
||||
- name: reload sshd
|
||||
ansible.builtin.systemd:
|
||||
name: sshd
|
||||
state: reloaded
|
||||
|
||||
- name: restart fail2ban
|
||||
ansible.builtin.systemd:
|
||||
name: fail2ban
|
||||
state: restarted
|
||||
|
||||
- name: restart pve-firewall
|
||||
ansible.builtin.command:
|
||||
cmd: pve-firewall restart
|
||||
changed_when: true
|
||||
@@ -0,0 +1,243 @@
|
||||
---
|
||||
# proxmox-hardening/tasks/main.yml
|
||||
# Mirrors the bash scripts in debian-configuration/proxmox/scripts/.
|
||||
# All tasks are idempotent — safe to re-run.
|
||||
|
||||
# ── APT repos ─────────────────────────────────────────────────────────────────
|
||||
|
||||
- name: Disable enterprise apt source (rename to .disabled)
|
||||
ansible.builtin.find:
|
||||
paths: /etc/apt/sources.list.d
|
||||
patterns: "*.sources,*.list"
|
||||
register: apt_sources
|
||||
when: proxmox_switch_to_nosub_repo
|
||||
tags: repos
|
||||
|
||||
- name: Disable enterprise apt sources
|
||||
ansible.builtin.command:
|
||||
cmd: "mv {{ item.path }} {{ item.path }}.disabled"
|
||||
loop: "{{ apt_sources.files | default([]) }}"
|
||||
when:
|
||||
- proxmox_switch_to_nosub_repo
|
||||
- item.path is not search('.disabled')
|
||||
register: mv_result
|
||||
changed_when: mv_result.rc == 0
|
||||
failed_when: mv_result.rc not in [0, 1]
|
||||
# Only disable files that actually reference enterprise.proxmox.com
|
||||
# (checked by grep in the shell — using shell here for grep pipe)
|
||||
tags: repos
|
||||
|
||||
- name: Write no-subscription apt source
|
||||
ansible.builtin.template:
|
||||
src: pve-no-subscription.sources.j2
|
||||
dest: /etc/apt/sources.list.d/pve-no-subscription.sources
|
||||
mode: "0644"
|
||||
notify: apt update
|
||||
when: proxmox_switch_to_nosub_repo
|
||||
tags: repos
|
||||
|
||||
# ── SSH hardening ─────────────────────────────────────────────────────────────
|
||||
|
||||
- name: Ensure sshd_config.d directory exists
|
||||
ansible.builtin.file:
|
||||
path: /etc/ssh/sshd_config.d
|
||||
state: directory
|
||||
mode: "0755"
|
||||
when: proxmox_harden_ssh
|
||||
tags: ssh
|
||||
|
||||
- name: Apply SSH hardening drop-in
|
||||
ansible.builtin.template:
|
||||
src: sshd-hardening.conf.j2
|
||||
dest: /etc/ssh/sshd_config.d/99-hardening.conf
|
||||
mode: "0644"
|
||||
validate: "sshd -t -f %s"
|
||||
notify: reload sshd
|
||||
when: proxmox_harden_ssh
|
||||
tags: ssh
|
||||
|
||||
- name: Install fail2ban
|
||||
ansible.builtin.apt:
|
||||
name: fail2ban
|
||||
state: present
|
||||
update_cache: false
|
||||
when: proxmox_harden_ssh
|
||||
tags: ssh
|
||||
|
||||
- name: Write fail2ban sshd jail
|
||||
ansible.builtin.template:
|
||||
src: fail2ban-sshd.local.j2
|
||||
dest: /etc/fail2ban/jail.d/sshd.local
|
||||
mode: "0644"
|
||||
notify: restart fail2ban
|
||||
when: proxmox_harden_ssh
|
||||
tags: ssh
|
||||
|
||||
- name: Enable and start fail2ban
|
||||
ansible.builtin.systemd:
|
||||
name: fail2ban
|
||||
enabled: true
|
||||
state: started
|
||||
when: proxmox_harden_ssh
|
||||
tags: ssh
|
||||
|
||||
# ── PVE datacenter firewall ───────────────────────────────────────────────────
|
||||
|
||||
- name: Ensure /etc/pve/firewall exists
|
||||
ansible.builtin.file:
|
||||
path: /etc/pve/firewall
|
||||
state: directory
|
||||
mode: "0750"
|
||||
when: proxmox_configure_firewall and proxmox_mgmt_cidr is defined
|
||||
tags: firewall
|
||||
|
||||
- name: Deploy datacenter firewall config
|
||||
ansible.builtin.template:
|
||||
src: cluster-fw.j2
|
||||
dest: /etc/pve/firewall/cluster.fw
|
||||
mode: "0640"
|
||||
notify: restart pve-firewall
|
||||
when: proxmox_configure_firewall and proxmox_mgmt_cidr is defined
|
||||
tags: firewall
|
||||
|
||||
# ── Unattended upgrades ───────────────────────────────────────────────────────
|
||||
|
||||
- name: Install unattended-upgrades
|
||||
ansible.builtin.apt:
|
||||
name: unattended-upgrades
|
||||
state: present
|
||||
when: proxmox_configure_unattended_upgrades
|
||||
tags: upgrades
|
||||
|
||||
- name: Write unattended-upgrades origins config
|
||||
ansible.builtin.template:
|
||||
src: unattended-upgrades.conf.j2
|
||||
dest: /etc/apt/apt.conf.d/51pve-unattended-upgrades.conf
|
||||
mode: "0644"
|
||||
when: proxmox_configure_unattended_upgrades
|
||||
tags: upgrades
|
||||
|
||||
- name: Write auto-upgrades config
|
||||
ansible.builtin.copy:
|
||||
content: |
|
||||
// Managed by ansible proxmox-hardening role
|
||||
APT::Periodic::Update-Package-Lists "1";
|
||||
APT::Periodic::Unattended-Upgrade "1";
|
||||
APT::Periodic::Download-Upgradeable-Packages "1";
|
||||
APT::Periodic::AutocleanInterval "7";
|
||||
dest: /etc/apt/apt.conf.d/20auto-upgrades
|
||||
mode: "0644"
|
||||
when: proxmox_configure_unattended_upgrades
|
||||
tags: upgrades
|
||||
|
||||
- name: Enable unattended-upgrades service
|
||||
ansible.builtin.systemd:
|
||||
name: unattended-upgrades
|
||||
enabled: true
|
||||
state: started
|
||||
when: proxmox_configure_unattended_upgrades
|
||||
tags: upgrades
|
||||
|
||||
# ── PVE admin user ────────────────────────────────────────────────────────────
|
||||
|
||||
- name: Check if named admin user already exists
|
||||
ansible.builtin.command:
|
||||
cmd: "pveum user list --output-format json"
|
||||
register: pve_users
|
||||
changed_when: false
|
||||
when: proxmox_admin_username is defined and proxmox_admin_username != ""
|
||||
tags: users
|
||||
|
||||
- name: Create named PVE admin user (one-time — password printed, must be changed on first login)
|
||||
ansible.builtin.command:
|
||||
cmd: >
|
||||
pveum user add {{ proxmox_admin_username }}@pve
|
||||
--password {{ lookup('password', '/dev/null length=24 chars=ascii_letters,digits') }}
|
||||
--comment "Named admin account, managed by ansible proxmox-hardening role"
|
||||
when:
|
||||
- proxmox_admin_username is defined
|
||||
- proxmox_admin_username != ""
|
||||
- pve_users.stdout is defined
|
||||
- proxmox_admin_username + "@pve" not in pve_users.stdout
|
||||
no_log: true
|
||||
tags: users
|
||||
|
||||
- name: Grant Administrator role to PVE admin user
|
||||
ansible.builtin.command:
|
||||
cmd: "pveum acl modify / --users {{ proxmox_admin_username }}@pve --roles Administrator"
|
||||
when:
|
||||
- proxmox_admin_username is defined
|
||||
- proxmox_admin_username != ""
|
||||
- pve_users.stdout is defined
|
||||
- proxmox_admin_username + "@pve" not in pve_users.stdout
|
||||
tags: users
|
||||
|
||||
# ── IPA sudo ──────────────────────────────────────────────────────────────────
|
||||
|
||||
- name: Check IPA enrollment
|
||||
ansible.builtin.stat:
|
||||
path: /etc/ipa/default.conf
|
||||
register: ipa_conf
|
||||
when: proxmox_setup_ipa_sudo
|
||||
tags: ipa
|
||||
|
||||
- name: Write admins NOPASSWD sudoers file
|
||||
ansible.builtin.copy:
|
||||
content: "%admins ALL=(root) NOPASSWD: ALL\n"
|
||||
dest: /etc/sudoers.d/admins-nopasswd
|
||||
mode: "0440"
|
||||
validate: "visudo -cf %s"
|
||||
when:
|
||||
- proxmox_setup_ipa_sudo
|
||||
- ipa_conf.stat.exists
|
||||
tags: ipa
|
||||
|
||||
- name: Find pvesh/qm/pct paths for proxmox sudoers
|
||||
ansible.builtin.command:
|
||||
cmd: "which {{ item }}"
|
||||
register: pve_tools
|
||||
loop: [pvesh, qm, pct]
|
||||
changed_when: false
|
||||
failed_when: false
|
||||
when:
|
||||
- proxmox_setup_ipa_sudo
|
||||
- ipa_conf.stat.exists
|
||||
tags: ipa
|
||||
|
||||
- name: Write admins proxmox tools sudoers file
|
||||
ansible.builtin.copy:
|
||||
content: >
|
||||
%admins ALL=(root) NOPASSWD:
|
||||
{{ pve_tools.results | map(attribute='stdout') | select | join(', ') }}
|
||||
dest: /etc/sudoers.d/admins-proxmox
|
||||
mode: "0440"
|
||||
validate: "visudo -cf %s"
|
||||
when:
|
||||
- proxmox_setup_ipa_sudo
|
||||
- ipa_conf.stat.exists
|
||||
- pve_tools.results | map(attribute='rc') | select('eq', 0) | list | length == 3
|
||||
tags: ipa
|
||||
|
||||
# ── Subscription nag ──────────────────────────────────────────────────────────
|
||||
|
||||
- name: Check if nag patch is needed
|
||||
ansible.builtin.shell:
|
||||
cmd: >
|
||||
grep -qF "data.status.toLowerCase() !== 'active'"
|
||||
/usr/share/javascript/proxmox-widget-toolkit/proxmoxlib.js
|
||||
register: nag_check
|
||||
changed_when: false
|
||||
failed_when: false
|
||||
when: proxmox_disable_nag
|
||||
tags: nag
|
||||
|
||||
- name: Patch subscription nag
|
||||
ansible.builtin.replace:
|
||||
path: /usr/share/javascript/proxmox-widget-toolkit/proxmoxlib.js
|
||||
regexp: "data\\.status\\.toLowerCase\\(\\) !== 'active'"
|
||||
replace: "false"
|
||||
backup: true
|
||||
when:
|
||||
- proxmox_disable_nag
|
||||
- nag_check.rc == 0
|
||||
tags: nag
|
||||
@@ -0,0 +1,27 @@
|
||||
# Managed by ansible proxmox-hardening role — do not edit by hand
|
||||
# Proxmox datacenter-level firewall (Stage 1: single-node management access only)
|
||||
# Stage 2 (Corosync/Ceph cluster) rules remain commented until needed.
|
||||
|
||||
[OPTIONS]
|
||||
enable: 1
|
||||
policy_in: DROP
|
||||
policy_out: ACCEPT
|
||||
|
||||
[IPSET mgmt]
|
||||
# Management subnet — SSH and web UI access only from here
|
||||
{{ proxmox_mgmt_cidr }}
|
||||
|
||||
[RULES]
|
||||
# Allow SSH from management network
|
||||
IN ACCEPT -source +mgmt -p tcp --dport 22 -log nolog
|
||||
# Allow PVE web UI from management network
|
||||
IN ACCEPT -source +mgmt -p tcp --dport 8006 -log nolog
|
||||
# Allow SPICE console from management network
|
||||
IN ACCEPT -source +mgmt -p tcp --dport 3128 -log nolog
|
||||
|
||||
# Stage 2 — Corosync (uncomment when clustering pve1 with additional nodes)
|
||||
# IN ACCEPT -source +mgmt -p udp --dport 5404:5405 -log nolog
|
||||
|
||||
# Stage 2 — Ceph (uncomment when Ceph OSD replication is active)
|
||||
# IN ACCEPT -source +mgmt -p tcp --dport 6800:7568 -log nolog
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
# Managed by ansible proxmox-hardening role — do not edit by hand
|
||||
[sshd]
|
||||
enabled = true
|
||||
port = ssh
|
||||
backend = systemd
|
||||
maxretry = {{ proxmox_fail2ban_maxretry }}
|
||||
bantime = {{ proxmox_fail2ban_bantime }}
|
||||
findtime = {{ proxmox_fail2ban_findtime }}
|
||||
@@ -0,0 +1,6 @@
|
||||
# Managed by ansible proxmox-hardening role — do not edit by hand
|
||||
Types: deb
|
||||
URIs: http://download.proxmox.com/debian/pve
|
||||
Suites: {{ ansible_distribution_release }}
|
||||
Components: pve-no-subscription
|
||||
Signed-By: /usr/share/keyrings/proxmox-archive-keyring.gpg
|
||||
@@ -0,0 +1,3 @@
|
||||
# Managed by ansible proxmox-hardening role — do not edit by hand
|
||||
PermitRootLogin prohibit-password
|
||||
PasswordAuthentication no
|
||||
@@ -0,0 +1,11 @@
|
||||
// Managed by ansible proxmox-hardening role — do not edit by hand
|
||||
Unattended-Upgrade::Origins-Pattern {
|
||||
"origin=Debian,codename={{ ansible_distribution_release }},label=Debian-Security";
|
||||
"origin=Debian,codename={{ ansible_distribution_release }}-security,label=Debian-Security";
|
||||
"origin=Proxmox";
|
||||
};
|
||||
|
||||
// Never auto-reboot a hypervisor. Check /var/run/reboot-required manually.
|
||||
Unattended-Upgrade::Automatic-Reboot "false";
|
||||
Unattended-Upgrade::Remove-Unused-Dependencies "false";
|
||||
Unattended-Upgrade::Remove-Unused-Kernel-Packages "false";
|
||||
@@ -0,0 +1,6 @@
|
||||
---
|
||||
# raspberrypi role defaults.
|
||||
|
||||
raspberrypi_setup_ipa_sudo: true
|
||||
raspberrypi_pin_docker_gid: true
|
||||
docker_access_gid: 50010
|
||||
@@ -0,0 +1,74 @@
|
||||
---
|
||||
# raspberrypi/tasks/main.yml
|
||||
# Post-IPA-enrollment setup for the Raspberry Pi edge node.
|
||||
# Mirrors debian-configuration/raspberrypi/scripts/.
|
||||
|
||||
# ── IPA sudo ──────────────────────────────────────────────────────────────────
|
||||
|
||||
- name: Check IPA enrollment
|
||||
ansible.builtin.stat:
|
||||
path: /etc/ipa/default.conf
|
||||
register: ipa_conf
|
||||
tags: ipa
|
||||
|
||||
- name: Write IPA admins sudoers file
|
||||
ansible.builtin.copy:
|
||||
content: "%admins ALL=(ALL) NOPASSWD:ALL\n"
|
||||
dest: /etc/sudoers.d/ipa-admins
|
||||
mode: "0440"
|
||||
validate: "visudo -cf %s"
|
||||
when:
|
||||
- raspberrypi_setup_ipa_sudo
|
||||
- ipa_conf.stat.exists
|
||||
tags: ipa
|
||||
|
||||
# ── Docker GID pinning ────────────────────────────────────────────────────────
|
||||
|
||||
- name: Check docker group GID
|
||||
ansible.builtin.command:
|
||||
cmd: "getent group docker"
|
||||
register: docker_group
|
||||
changed_when: false
|
||||
failed_when: false
|
||||
when: raspberrypi_pin_docker_gid
|
||||
tags: docker
|
||||
|
||||
- name: Pin docker group GID to IPA docker-access GID
|
||||
block:
|
||||
- name: Stop docker service and socket
|
||||
ansible.builtin.systemd:
|
||||
name: "{{ item }}"
|
||||
state: stopped
|
||||
loop:
|
||||
- docker.service
|
||||
- docker.socket
|
||||
|
||||
- name: Remove stale docker socket
|
||||
ansible.builtin.file:
|
||||
path: /var/run/docker.sock
|
||||
state: absent
|
||||
|
||||
- name: Reassign docker group GID (non-unique — SSSD already owns this GID)
|
||||
ansible.builtin.command:
|
||||
cmd: "groupmod --non-unique -g {{ docker_access_gid }} docker"
|
||||
changed_when: true
|
||||
|
||||
- name: Start docker socket and service
|
||||
ansible.builtin.systemd:
|
||||
name: "{{ item }}"
|
||||
state: started
|
||||
loop:
|
||||
- docker.socket
|
||||
- docker.service
|
||||
|
||||
- name: Verify socket GID
|
||||
ansible.builtin.command:
|
||||
cmd: "stat -c '%g' /var/run/docker.sock"
|
||||
register: socket_gid
|
||||
changed_when: false
|
||||
failed_when: socket_gid.stdout != docker_access_gid | string
|
||||
when:
|
||||
- raspberrypi_pin_docker_gid
|
||||
- docker_group.rc == 0
|
||||
- docker_group.stdout.split(':')[2] != docker_access_gid | string
|
||||
tags: docker
|
||||
Reference in New Issue
Block a user