--- # Materializes the SSH key the k3s plays authenticate with, from Vault # (homelab/ci/ssh-k3s) onto the controller. Imported as the first play of both # playbooks/k3s.yml and playbooks/k3s_maintenance.yml — a separate file rather # than copied into each, since both target k3s_cluster and both would # otherwise depend on a file only the other one creates. # # This is the same shape CI already uses for the Unraid key: # .forgejo/workflows/deploy.yml fetches homelab/ci/ssh, writes it to disk and # points ANSIBLE_PRIVATE_KEY_FILE at it. Vault is the one place secrets live, # so a manual k3s run shouldn't need the key hand-loaded into ssh-agent first # — which was the only way it worked before. # # hosts: k3s_cluster, not localhost, and deliberately so: the implicit # localhost is not a member of `all`, so it doesn't inherit group_vars/all.yml # — where vault_addr, vault_kv_mount and vault_auth_method live. Targeting the # group picks those up along with k3s_ssh_key_vault_path/_local_path from # group_vars/k3s_cluster.yml. Nothing here connects to a Pi (gather_facts is # off and every task is delegated or connectionless), which is the whole # point: at this stage the key isn't on disk yet. # # become: false on the delegated tasks because the k3s_cluster group sets # ansible_become: true for the Pis (inventory/hosts.yml) — without it these # would try to sudo on the controller. - name: Fetch the k3s SSH deploy key from Vault hosts: k3s_cluster gather_facts: false tasks: # The escape hatch, and the reason it's a flag rather than just pointing # ansible_ssh_private_key_file somewhere else: if Vault is unreachable, # overriding the key path alone doesn't help — this play would still fail # before the first real one runs. Skipping it is the only thing that lets # a run proceed on a local key. Both overrides together: # # ansible-playbook playbooks/k3s.yml \ # -e k3s_ssh_key_fetch=false \ # -e ansible_ssh_private_key_file=~/.ssh/k3s_ansible - name: Fetch and write the key when: k3s_ssh_key_fetch | default(true) | bool block: # run_once because the key is per-cluster, not per-host: one Vault # read for the whole group. Facts set by a run_once task apply to # every host in the play, but nothing outside this play needs it. - name: Look up the k3s SSH deploy key from Vault ansible.builtin.set_fact: k3s_ssh_key_secret: >- {{ lookup('community.hashi_vault.vault_kv2_get', k3s_ssh_key_vault_path, engine_mount_point=vault_kv_mount, url=vault_addr, auth_method=vault_auth_method, role_id=vault_role_id | default(omit), secret_id=vault_secret_id | default(omit)).secret }} run_once: true no_log: true - name: Ensure the local key directory exists ansible.builtin.file: path: "{{ k3s_ssh_key_local_path | dirname }}" state: directory mode: "0700" delegate_to: localhost become: false run_once: true # trim + an explicit trailing newline: OpenSSH rejects a key file # whose final line isn't terminated, and `vault kv put # PRIVATE_KEY=@file` is as likely to have stored one with trailing # whitespace as not. 0600 for the same reason — ssh refuses a key file # that's group- or world-readable. Both fail at connection time rather # than here, which is a much less obvious error to read. - name: Write the private key to the controller ansible.builtin.copy: dest: "{{ k3s_ssh_key_local_path }}" content: "{{ k3s_ssh_key_secret.PRIVATE_KEY | trim }}\n" mode: "0600" delegate_to: localhost become: false run_once: true no_log: true