This repository has been archived on 2026-07-30. You can view files and clone it. You cannot open issues or pull requests or push a commit.
Files
nixos/ansible/roles/proxmox-hardening/tasks/main.yml
T
beatzaplentyandClaude Sonnet 4.6 f85c65870f 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
2026-07-30 07:07:47 +10:00

244 lines
7.9 KiB
YAML

---
# 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