216 lines
7.7 KiB
YAML
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
|