Dotfiles/setup/modules/FreeipaAnsible/ansible/deploy-ansipa-git-pull.yml

216 lines
7.7 KiB
YAML

---
# deploy-ansipa-git-pull.yml — bootstrap the signed git-based policy puller.
#
# Replaces the old unsigned SMB-policystore auto-sync with a pull mechanism
# where every applied commit is GPG-signature-verified and checked for
# fast-forward (no rollback/replay) before anything runs as root. See
# docs/md/freeipa-ansible.md, "Git-based policy distribution", for the full
# design and threat model.
#
# Prerequisites:
# - deploy-ansipa-policies.yml already run on this host (installs
# ansipa-enforce-policies.sh + the Ansible-deployed policies.d baseline
# that this system falls back to before the first successful pull).
# - The ANSIPA git server is up (image/ansipa-git-server-setup.sh, run on
# the FreeIPA container) and this node's read-only deploy key has been
# registered there:
# docker exec freeipa ansipa-git-add-deploy-key.sh "ssh-ed25519 AAAA... <fqdn>"
# - The git server's SSH host key has been fetched and verified OUT OF BAND
# (never trust an unauthenticated ssh-keyscan) — pass it as
# ansipa_git_host_key. This is what actually stops a DNS-spoof-to-attacker
# scenario; GPG signing is what stops tampered content even if transport
# trust were somehow broken.
#
# Required variables (pass via -e, host_vars, or an Ansible Vault file):
# ansipa_git_host FQDN/IP of the ANSIPA git server
# ansipa_git_port sshd port for the git service (default 2222)
# ansipa_git_host_key exact known_hosts line(s) for ansipa_git_host,
# e.g. "[host]:2222 ssh-ed25519 AAAA..."
# ansipa_git_deploy_key this node's PRIVATE deploy key (vault this)
# ansipa_gpg_pubkey armored GPG public key that signs policy commits
#
# Optional:
# ansipa_seed_commit commit SHA to seed last-applied-commit with, so an
# already-configured fleet doesn't re-apply the
# entire policy history on first pull.
# ansipa_ntfy_url alert destination (curl -d) for verify/apply failures
#
# Usage:
# ansible-playbook -i inventory deploy-ansipa-git-pull.yml \
# -e ansipa_git_host=ipa.corp.example.com -e ansipa_git_port=2222 \
# -e @vault-ansipa-git-pull.yml
- name: Bootstrap ANSIPA signed git policy puller
hosts: all
become: yes
vars:
ansipa_git_port: 2222
ansipa_state_dir: /var/lib/ansipa
tasks:
- name: Require the git-pull identity variables
assert:
that:
- ansipa_git_host is defined
- ansipa_git_host_key is defined
- ansipa_git_deploy_key is defined
- ansipa_gpg_pubkey is defined
fail_msg: >-
ansipa_git_host, ansipa_git_host_key, ansipa_git_deploy_key and
ansipa_gpg_pubkey are all required — see the header of this playbook.
- name: Install git
package:
name: git
state: present
- name: Create _ansipa service account (nologin, home = state dir)
user:
name: _ansipa
system: yes
shell: /usr/sbin/nologin
home: "{{ ansipa_state_dir }}"
create_home: no
password_lock: yes
- name: Create ansipa state/ssh/gnupg directories
file:
path: "{{ item.path }}"
state: directory
owner: _ansipa
group: _ansipa
mode: "{{ item.mode }}"
loop:
- { path: "{{ ansipa_state_dir }}", mode: "0700" }
- { path: "{{ ansipa_state_dir }}/.ssh", mode: "0700" }
- { path: "{{ ansipa_state_dir }}/.gnupg", mode: "0700" }
- name: Pin the ANSIPA git server host key (StrictHostKeyChecking=yes — the
transport-level defense against a spoofed/attacker-controlled endpoint)
copy:
dest: "{{ ansipa_state_dir }}/.ssh/known_hosts"
content: "{{ ansipa_git_host_key }}\n"
owner: _ansipa
group: _ansipa
mode: "0644"
- name: Install this node's read-only deploy key
copy:
dest: "{{ ansipa_state_dir }}/.ssh/deploy_key"
content: "{{ ansipa_git_deploy_key }}"
owner: _ansipa
group: _ansipa
mode: "0600"
- name: Write SSH client config for the ansipa-git alias
copy:
dest: "{{ ansipa_state_dir }}/.ssh/config"
owner: _ansipa
group: _ansipa
mode: "0600"
content: |
Host ansipa-git
HostName {{ ansipa_git_host }}
Port {{ ansipa_git_port }}
User git
IdentityFile {{ ansipa_state_dir }}/.ssh/deploy_key
IdentitiesOnly yes
UserKnownHostsFile {{ ansipa_state_dir }}/.ssh/known_hosts
StrictHostKeyChecking yes
- name: Write the GPG public key that signs policy commits
copy:
dest: "{{ ansipa_state_dir }}/policy-signing-pubkey.asc"
content: "{{ ansipa_gpg_pubkey }}"
owner: _ansipa
group: _ansipa
mode: "0644"
- name: Import the signing public key into _ansipa's keyring
become: yes
become_user: _ansipa
environment:
GNUPGHOME: "{{ ansipa_state_dir }}/.gnupg"
HOME: "{{ ansipa_state_dir }}"
command: gpg --batch --import {{ ansipa_state_dir }}/policy-signing-pubkey.asc
register: _gpg_import
changed_when: "'imported' in _gpg_import.stderr or 'unchanged' in _gpg_import.stderr"
- name: Trust the imported signing key (single-key distribution channel,
not a multi-party web of trust — ultimate trust avoids a manual prompt)
become: yes
become_user: _ansipa
environment:
GNUPGHOME: "{{ ansipa_state_dir }}/.gnupg"
HOME: "{{ ansipa_state_dir }}"
shell: |
set -euo pipefail
gpg --batch --with-colons --list-keys | awk -F: '/^fpr:/{print $10 ":6:"}' \
| gpg --batch --import-ownertrust
changed_when: false
- name: Bootstrap the bare mirror clone (idempotent — skipped if already present)
become: yes
become_user: _ansipa
environment:
HOME: "{{ ansipa_state_dir }}"
command: git clone --mirror ansipa-git:ansipa-policy.git {{ ansipa_state_dir }}/repo.git
args:
creates: "{{ ansipa_state_dir }}/repo.git"
- name: Seed last-applied-commit (avoids re-applying full history on an
already-configured fleet); leave absent to apply from the very first commit
copy:
dest: "{{ ansipa_state_dir }}/last-applied-commit"
content: "{{ ansipa_seed_commit }}\n"
owner: _ansipa
group: _ansipa
mode: "0600"
force: no
when: ansipa_seed_commit is defined
- name: Write /etc/ansipa-pull.conf
copy:
dest: /etc/ansipa-pull.conf
mode: "0644"
content: |
# ansipa-pull configuration — managed by deploy-ansipa-git-pull.yml
ANSIPA_GIT_REMOTE=origin
ANSIPA_GIT_BRANCH=main
{% if ansipa_ntfy_url is defined %}ANSIPA_NTFY_URL={{ ansipa_ntfy_url }}{% endif %}
- name: Deploy ansipa-pull-apply.sh
copy:
src: ansipa-pull-apply.sh
dest: /usr/local/bin/ansipa-pull-apply.sh
mode: "0755"
- name: Deploy scoped sudoers drop-in (one exact command, no arguments)
copy:
src: ansipa-git-pull-sudoers
dest: /etc/sudoers.d/ansipa-git-pull
owner: root
group: root
mode: "0440"
validate: "visudo -cf %s"
- name: Install ansipa-pull systemd service + timer
copy:
src: "{{ item }}"
dest: "/etc/systemd/system/{{ item }}"
mode: "0644"
loop:
- ansipa-pull.service
- ansipa-pull.timer
- name: Reload systemd
command: systemctl daemon-reload
- name: Enable and start the ansipa-pull timer
systemd:
name: ansipa-pull.timer
enabled: yes
state: started