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