Das Ansible-Playbook, das auf jedem neuen Server läuft
Ich habe schon einmal über die Checklist geschrieben, die ich bei jedem neuen Server durchgehe: ein Non-Root-Benutzer, SSH nur mit Key, eine Default-Deny-Firewall, Fail2ban, unbeaufsichtigte Updates. Das von Hand zu machen dauert unter einer Stunde und ist nicht schwer. Aber "unter einer Stunde, von Hand, für jeden Server" summiert sich schnell, sobald man mehr als zwei oder drei davon betreut, und manuelle Schritte sind genau die Stelle, an der sich kleine Inkonsistenzen einschleichen, ein Server bekommt MaxAuthTries 3, ein anderer nicht, weil derjenige, der ihn aufgesetzt hat, an dem Tag in Eile war.
Die Lösung ist, die Checklist in ein Playbook zu verwandeln. Dieselben Schritte, dieselbe Reihenfolge, jedes Mal, und idempotent genug, dass ein erneuter Lauf auf einem bereits konfigurierten Server nichts verändert.
Das Playbook
---
- name: Baseline-Härtung für einen neuen Server
hosts: new_servers
become: true
vars:
admin_user: deploy
ssh_public_key: "{{ lookup('file', '~/.ssh/id_ed25519.pub') }}"
tasks:
- name: Admin-Benutzer anlegen
user:
name: "{{ admin_user }}"
groups: sudo
shell: /bin/bash
create_home: true
- name: SSH-Key für Admin-Benutzer hinterlegen
authorized_key:
user: "{{ admin_user }}"
key: "{{ ssh_public_key }}"
- name: Root-Login und Passwort-Auth deaktivieren
lineinfile:
path: /etc/ssh/sshd_config
regexp: "{{ item.regexp }}"
line: "{{ item.line }}"
loop:
- { regexp: '^#?PermitRootLogin', line: 'PermitRootLogin no' }
- { regexp: '^#?PasswordAuthentication', line: 'PasswordAuthentication no' }
- { regexp: '^#?MaxAuthTries', line: 'MaxAuthTries 3' }
notify: sshd neu starten
- name: UFW und Fail2ban installieren
apt:
name: [ufw, fail2ban]
state: present
update_cache: true
- name: UFW-Standardregeln setzen
ufw:
direction: "{{ item.direction }}"
policy: "{{ item.policy }}"
loop:
- { direction: incoming, policy: deny }
- { direction: outgoing, policy: allow }
- name: Benötigte Ports erlauben
ufw:
rule: allow
port: "{{ item }}"
proto: tcp
loop: ['22', '80', '443']
- name: UFW aktivieren
ufw:
state: enabled
- name: Fail2ban aktivieren
systemd:
name: fail2ban
enabled: true
state: started
- name: unattended-upgrades installieren
apt:
name: unattended-upgrades
state: present
handlers:
- name: sshd neu starten
systemd:
name: ssh
state: restarted
Ausführen
Ein frischer Server kommt ins Inventory, und das Playbook läuft gezielt gegen genau diesen Host:
ansible-playbook -i inventory.ini baseline.yml -l new-server-01
Der erste Lauf macht die ganze Arbeit, legt den Benutzer an, sperrt SSH ab, richtet die Firewall ein. Der Handler startet sshd nur neu, wenn sich die Config tatsächlich geändert hat, das passiert also nur beim allerersten Lauf, jeder Lauf danach ist eine Bestätigung, dass nichts abgedriftet ist.
Warum Idempotenz der eigentliche Punkt ist
Der eigentliche Wert liegt nicht in der gesparten Zeit am ersten Tag, die Befehle von Hand zu tippen ist nicht langsam. Er liegt darin, dass ich sechs Monate später, wenn ich nicht mehr sicher bin, ob ein bestimmter Server die volle Behandlung bekommen hat oder während eines Incidents in Eile aufgesetzt wurde, das Playbook einfach noch einmal laufen lassen kann. Wenn alles schon passt, meldet Ansible null Änderungen, und ich mache weiter. Fehlt etwas, wird es direkt behoben, ohne dass ich mich erinnern muss, welcher der vier oder fünf manuellen Schritte ausgelassen wurde.
Dieses Playbook ist bewusst klein gehalten. Es installiert keine Anwendungs-Stacks und konfiguriert nichts Projektspezifisches, es ist die Basis, auf der jeder Server steht, bevor irgendetwas anderes darauf aufgebaut wird. Diese Trennung von Anwendungs-Playbooks hält die Basis stabil, und eine Baseline, die sich selten ändert, ist eine, der man vertrauen kann, ohne sie bei jedem Mal neu zu lesen.