106 lines
3.5 KiB
YAML
106 lines
3.5 KiB
YAML
---
|
|
# One-job playbook: connect as root, create the automation user that every
|
|
# later run uses — key-only auth, passwordless sudo — verify it actually
|
|
# works, then lock sshd down to keys. Run once per fresh VPS:
|
|
#
|
|
# ansible-playbook bootstrap.yml
|
|
#
|
|
# It prompts for the root SSH password (needs sshpass on the control
|
|
# node). If root already authenticates with your key, just press Enter.
|
|
#
|
|
# Afterwards set `ansible_user: ansible` in inventory.yml and never
|
|
# connect as root again. Overridable with -e:
|
|
# bootstrap_user=ansible
|
|
# bootstrap_ssh_public_key_file=~/.ssh/id_ed25519.pub
|
|
# bootstrap_harden_ssh=true
|
|
#
|
|
# Safe by construction: play 2 logs in AS the new user and proves key +
|
|
# sudo work; sshd is only hardened in play 3, which never runs if play 2
|
|
# fails. Play 3 disables root SSH login entirely — from then on the only
|
|
# way in is the automation user's key (console access via the RackNerd
|
|
# panel remains the break-glass path).
|
|
|
|
- name: Create the automation user (connects as root)
|
|
hosts: vpnhub
|
|
gather_facts: false
|
|
vars_prompt:
|
|
- name: bootstrap_root_password
|
|
prompt: "Root SSH password (Enter if root already uses your key)"
|
|
private: true
|
|
default: ""
|
|
vars:
|
|
ansible_user: root # play var — outranks inventory on purpose
|
|
ansible_password: "{{ bootstrap_root_password }}"
|
|
bootstrap_user: ansible
|
|
bootstrap_ssh_public_key_file: "~/.ssh/togo-ansible.pub"
|
|
tasks:
|
|
- name: Create the user (no password hash set, account password locked)
|
|
ansible.builtin.user:
|
|
name: "{{ bootstrap_user }}"
|
|
shell: /bin/bash
|
|
create_home: true
|
|
password_lock: true
|
|
|
|
- name: Install the SSH public key
|
|
ansible.posix.authorized_key:
|
|
user: "{{ bootstrap_user }}"
|
|
key: "{{ lookup('file', bootstrap_ssh_public_key_file) }}"
|
|
|
|
- name: Grant passwordless sudo
|
|
ansible.builtin.copy:
|
|
dest: "/etc/sudoers.d/{{ bootstrap_user }}"
|
|
content: "{{ bootstrap_user }} ALL=(ALL) NOPASSWD:ALL\n"
|
|
owner: root
|
|
group: root
|
|
mode: "0440"
|
|
validate: /usr/sbin/visudo -cf %s
|
|
|
|
- name: Prove the new user works before touching sshd
|
|
hosts: vpnhub
|
|
gather_facts: false
|
|
become: true
|
|
vars:
|
|
bootstrap_user: ansible
|
|
ansible_user: "{{ bootstrap_user }}"
|
|
tasks:
|
|
- name: Key login and passwordless sudo both work
|
|
ansible.builtin.command: whoami
|
|
register: bootstrap_whoami
|
|
changed_when: false
|
|
failed_when: bootstrap_whoami.stdout != "root"
|
|
|
|
- name: Lock sshd down to key-only auth, no root login
|
|
hosts: vpnhub
|
|
gather_facts: false
|
|
become: true
|
|
vars:
|
|
bootstrap_user: ansible
|
|
ansible_user: "{{ bootstrap_user }}"
|
|
bootstrap_harden_ssh: true
|
|
handlers:
|
|
- name: restart sshd
|
|
ansible.builtin.service:
|
|
name: ssh
|
|
state: restarted
|
|
tasks:
|
|
- name: Key-only sshd drop-in
|
|
# Named 01-* on purpose: sshd honors the FIRST value it reads per
|
|
# keyword, and cloud-init images ship 50-cloud-init.conf that may
|
|
# set PasswordAuthentication yes — we must sort before it.
|
|
ansible.builtin.copy:
|
|
dest: /etc/ssh/sshd_config.d/01-vpnhub.conf
|
|
content: |
|
|
PasswordAuthentication no
|
|
KbdInteractiveAuthentication no
|
|
PermitRootLogin no
|
|
owner: root
|
|
group: root
|
|
mode: "0644"
|
|
when: bootstrap_harden_ssh | bool
|
|
notify: restart sshd
|
|
|
|
- name: Validate full sshd config before the restart fires
|
|
ansible.builtin.command: /usr/sbin/sshd -t
|
|
when: bootstrap_harden_ssh | bool
|
|
changed_when: false
|