Add the prepare-server playbook

Two plays: bootstrap as the provider account (ubuntu, over bootstrap.pem), then
everything else as the user that first play just created. The split is forced
rather than stylistic -- the second play logs in as that user, so its
authorized_keys entry and its NOPASSWD sudoers entry both have to exist before
it starts. That is why the key tasks live in the 'user' role and 'wheel' runs
in the first play: move either into play 2 and it fails with an auth error or
"Missing sudo password".

Both accounts and both keys are variables. The inventory sits beside the
playbook and is read via ansible.cfg, which Ansible picks up from the current
working directory rather than the playbook's, so it has to be run from this
directory. bootstrap.pem is gitignored -- it is the provider key for the host.
This commit is contained in:
tanshu committed 2026-10-15 21:24:32 +05:30
1 parent f2d1b606ce
commit 97d6892c83
11 files changed
+445

No files matched your search

+88
View File
@@ -0,0 +1,88 @@
---
# Tasks for the user role.
#
# Runs in the first (bootstrap) play, against the provider's bootstrap account
# ({{ bootstrap_user }}), and does the two things that must happen before
# anything else can: create the account this server is being prepared for
# ({{ user }}), and install the public keys that make it reachable. Every
# later task logs in as {{ user }} rather than as the bootstrap account, so
# without those keys the second play cannot connect at all.
#
# Authentication is key based only: no password is set, which leaves the
# account locked for password logins. The shell, group membership and the rest
# of the account's configuration belong to the wheel/zsh roles. Setting a
# shell here would fight the zsh role and change it back on every run, so this
# role deliberately does not touch it.
- name: Create the desired user
ansible.builtin.user:
name: "{{ user }}"
create_home: true
state: present
- name: Ensure the user's SSH directory exists
ansible.builtin.file:
path: "/home/{{ user }}/.ssh"
state: directory
owner: "{{ user }}"
group: "{{ user }}"
mode: "0700"
- name: Ensure authorized_keys file exists
ansible.builtin.copy:
content: ""
dest: "/home/{{ user }}/.ssh/authorized_keys"
force: false
owner: "{{ user }}"
group: "{{ user }}"
mode: "0644"
# The second play logs in as {{ user }} with the private half of this key on
# the control node, so this entry is what makes that login possible. It is
# installed here, during the bootstrap play, rather than being left to depend
# on one of the named keys below happening to be the control node's own.
#
# lineinfile rather than ansible.builtin.authorized_key: the latter redirects
# to ansible.posix, whose 2.1.0 module imports a deprecated helper and emits a
# deprecation warning on every run. This is the same guarantee without it.
- name: Authorize the control node's key for the new user
ansible.builtin.lineinfile:
dest: "/home/{{ user }}/.ssh/authorized_keys"
line: "{{ lookup('file', user_key ~ '.pub') | trim }}"
create: true
owner: "{{ user }}"
group: "{{ user }}"
mode: "0644"
- name: Check Terminus public key
ansible.builtin.shell: "grep -c \"{{ terminus_key }}\" /home/{{ user }}/.ssh/authorized_keys || true"
register: terminuskey_test
changed_when: false
- name: Add Terminus public key
ansible.builtin.lineinfile:
dest: "/home/{{ user }}/.ssh/authorized_keys"
line: "{{ terminus_key }}"
when: terminuskey_test.stdout == "0"
- name: Check clair public key
ansible.builtin.shell: "grep -c \"{{ clair_key }}\" /home/{{ user }}/.ssh/authorized_keys || true"
register: clairkey_test
changed_when: false
- name: Add clair public key
ansible.builtin.lineinfile:
dest: "/home/{{ user }}/.ssh/authorized_keys"
line: "{{ clair_key }}"
when: clairkey_test.stdout == "0"
- name: Check Anjin public key
ansible.builtin.shell: "grep -c \"{{ anjin_key }}\" /home/{{ user }}/.ssh/authorized_keys || true"
register: anjin_key_test
changed_when: false
- name: Add Anjin public key
ansible.builtin.lineinfile:
dest: "/home/{{ user }}/.ssh/authorized_keys"
line: "{{ anjin_key }}"
when: anjin_key_test.stdout == "0"