Ansible-Tutorial: Setup von User-Accounts, SSH und Firewall mit Ansible

Im ersten Teil dieser Tutorial-Serie habe ich euch gezeigt, wie ich mit Ansible auf einem Root-Server von Hetzner das Betriebssystem-Image installiert habe. Im zweiten Teil des Ansible-Tutorials geht es um das „Bootstrapping“ des Servers. Unter dem Begriff „Bootstrapping“ fasse ich alle Aktionen zusammen, die für alle Server gleich sein werden, egal für welche Aufgabe ein Server eingesetzt werden wird. Im Einzelnen werde ich:

  • Patches einspielen,
  • den Hostnamen festlegen,
  • eine Hosts-Datei anlegen,
  • einen weiteren User anlegen, um mich nicht als root einloggen zu müssen,
  • die SSH-Konfiguration optimieren,
  • ein paar Bash-Aliase anlegen,
  • einige hilfreiche Kommandozeilen-Tools installieren und
  • die Firewall konfigurieren.

Den Code zum Artikel findet ihr in meinem GitHub-Repository unter https://gitlab.com/SvenWoltmann/happycoders-tutorial-server-setup.

Zusätzlich zum Artikel gibt es auch wieder ein Video-Tutorial: Setup von User-Accounts, SSH und Firewall mit Ansible

Die Artikelserie ist in folgende vier Teile aufgeteilt:

  1. Setup eines Root-Servers mit Ansible
  2. Setup von User-Accounts, SSH und Firewall mit Ansible
  3. Setup von Docker, MySQL und WordPress mit Ansible
  4. Setup von HAProxy und eines HTTPS-Zertifikats von Let’s Encrypt mit Ansible

Bootstrap-Ansible-Playbook

Als erstes erstelle ich im Projektverzeichnis die Datei bootstrap.yml mit zunächst folgendem Inhalt:

---
- hosts: "{{ target }}"
remote_user: root
roles:
- apt_upgrade
- hostnames
- users
- ssh

Der Unterschied zu den Playbooks des vorherigen Artikels ist, dass ich die Zeile gather_facts: false weglasse. Da der Default-Wert true ist, wird Ansible vor dem Ausführen der ersten Rolle die Fakten des Zielsystems (bspw. dessen Betriebssystem-Version) zusammentragen. Dies ist nur möglich, wenn auf dem Zielsystem bereits Python installiert ist. Das hatte ich im vorherigen Artikel als letzten Schritt erledigt. Die aufgelisteten Rollen erkläre ich im Folgenden Schritt für Schritt.

Patches einspielen

Als erstes möchte ich den Server auf den aktuellsten Stand bringen, in dem ich alle verfügbare Patches einspiele. Dazu erstelle ich die Rolle apt_upgrade, für die in der Datei roles/apt_upgrade/tasks/main.yml nur ein einzelner Task definiert ist:

---
- name: Update and upgrade apt packages
apt:
upgrade: yes
update_cache: yes

Ich verwende das Ansible-Modul apt, wobei update_cache: yes das Äquivalent ist für apt update und upgrade: yes für apt upgrade steht. Dieser Task ist das Äquivalent zur Ausführung von apt update && apt-upgrade auf der Kommandozeile.

Hostname festlegen

Nun möchte ich sicherstellen, dass die Datei /etc/hostname den korrekten Hostnamen enthält. Das sollte zwar das installimage-Skript erledigt haben, aber es schadet nichts, das an dieser Stelle noch einmal zu prüfen und ggf. zu korrigieren. Letztendlich sollte dieses Ansible-Playbook auch auf Servern anderer Hoster laufen, auf denen die hostname-Datei evtl. nicht bereits bei der Provisionierung gesetzt wird. Dazu lege ich die Rolle hostnames an in Form der Datei roles/hostnames/tasks/main.yml mit folgender Task-Definition:

---
- name: Set hostname
hostname:
name: "{{ inventory_hostname }}"

Der Task verwendet das Ansible-Modul hostname, um zum einen das gleichnamige Kommando auszuführen, um den Hostnamen zu ändern, und zum anderen den Hostnamen in die Datei /etc/hostname zu schreiben, so dass dieser auch bei einem Serverneustart erhalten bleibt.

Hosts-Datei anlegen

Außerdem möchte ich alle relevanten Servernamen in die /etc/hosts-Datei schreiben. Im Moment ist das nur einer, doch sobald ich mehrere Server habe, wird das wichtig sein. Ich kopiere mir die Datei /etc/hosts des Servers in mein lokales Projektverzeichnis nach roles/hostnames/files/hosts und passe lediglich die Kommentare leicht an, so dass die Datei folgenden Inhalt hat:

# IPv4
127.0.0.1 localhost.localdomain localhost
46.4.99.9 happy1.happycoders.eu happy1

# IPv6
::1 ip6-localhost ip6-loopback
fe00::0 ip6-localnet
ff00::0 ip6-mcastprefix
ff02::1 ip6-allnodes
ff02::2 ip6-allrouters
ff02::3 ip6-allhosts
2a01:4f8:140:9090::2 happy1.happycoders.eu happy1

Zum Kopieren der Datei auf den Server verwende ich das Ansible-Modul copy und füge entsprechend folgenden Eintrag in die zuvor angelegten Rollen-Datei roles/hostnames/tasks/main.yml ein:

- name: Copy "hosts" file
copy:
src: hosts
dest: /etc/hosts
owner: root
group: root
mode: 0644

Mit src wird die Quelldatei relativ zum files/-Verzeichnis angegeben, mit dest der Zielpfad auf dem Server. Die restlichen drei Parameter legen den Besitzer und die Gruppe der Datei sowie die Bits für den Dateimodus fest.

User anlegen

Aus Sicherheitsgründen möchte ich mich in Zukunft nicht als root-User mit dem Server verbinden, daher lege ich einen User sven an und füge diesen zur Gruppe sudo hinzu. Während der Entwicklungsphase möchte ich allerdings nicht immer wieder für sudo mein Passwort eingeben müssen. Dafür erstelle ich eine zusätzliche Gruppe sudo-nopasswd, füge den User sven hinzu und erlaube allen Usern dieser Gruppe, sudo ohne Passwort auszuführen. Das alles mache ich wie immer mit Ansible, d. h. ich lege die Rolle users an und die Datei roles/hostnames/users/main.yml mit folgenden Tasks:

Task 1 – sudo installieren:

- name: Install sudo
apt:
name: sudo

Task 2 – User sven anlegen:

- name: Create "sven" user
user:
name: sven
shell: /bin/bash
uid: 1000
group: users
groups: sudo
append: true # --> user is not removed from any other group
password: $6$2i/vlnXuBj/m8A$y9xNcMAJQstggFMSWYbVeJStvvDpk4s4jScPv074Fb94jSfCbxyWSPz.tmMPQ6Qiy6mnpn07SQRTe6Sex4Pfi/

Den User erstelle ich mit dem Ansible-Modul user und verwende dabei folgende Parameter:

  • Mit name lege ich den Benutzernamen fest.
  • Die Benutzer-Shell wird mit shell festgelegt.
  • Ich lege mit uid eine fixe User-ID fest.
  • Mit group lege ich fest, welches die Hauptgruppe des Benutzers wird.
  • Mit groups gebe ich an, dass der User zusätzlich in der sudo-Gruppe enthalten sein soll.
  • Wichtig ist append: true anzugeben, da der User sonst aus Gruppen, denen er evtl. in anderen Playbooks hinzugefügt wurde, wieder entfernt werden würde.
  • Mit password gebe ich mein Passwort in verschlüsselter Form an. Dieses habe ich mit dem Kommando mkpasswd -m sha-512 generiert.

Task 3 – Gruppe sudo-nopasswd anlegen:

- name: Create "sudo-nopasswd" group
group:
name: sudo-nopasswd

Hier verwende ich das Ansible-Modul group; dieses legt die unter name bezeichnete Gruppe an.

Task 4 – User sven zur Gruppe sudo-nopasswd hinzufügen:

- name: Add user "sven" to "sudo-nopasswd" group
user:
name: sven
groups: sudo-nopasswd
append: true # --> user is not removed from any other group
when: passwordless_sudo is defined and passwordless_sudo == true

Ich verwende hier erneut das user-Modul, welches ich weiter oben bereits erklärt habe. Mit when lege ich fest, dass die Gruppenzuordnung nur dann erfolgen soll, wenn die Variable passwordless_sudo vorhanden und auf true gesetzt ist. Somit kann ich nach der Entwicklungsphase sehr leicht die Passwort-Abfrage für sudo wieder aktivieren, indem ich die Variable entferne oder auf false setze. In diesem Fall wird Task 4 zwar nicht mehr ausgeführt, entfernt wird der User dadurch aber nicht aus der Gruppe. Dies erledige ich in Task 5.

Task 5 – User sven aus Gruppe sudo-nopasswd entfernen:

- name: Remove user "sven" from "sudo-nopasswd" group
shell: /usr/sbin/delgroup sven sudo-nopasswd
when: not (passwordless_sudo is defined and passwordless_sudo == true)
ignore_errors: yes

Dies ist leider mit dem user-Modul nicht möglich, daher verwende ich dafür das shell-Modul und das delgroup-Linux-Kommando. Wichtig ist hier die Zeile ignore_errors: yes anzugeben, da das Playbook ansonsten an dieser Stelle mit einer Fehlermeldung abbrechen würde, wenn der User nicht in der Gruppe ist, aus der er entfernt werden soll.

Task 6 – Festlegen, dass User der Gruppe sudo-nopasswd das Kommando sudo ohne Passwort ausführen dürfen:

- name: Add "sudo-nopasswd" group to "sudoers" file
lineinfile:
dest: /etc/sudoers
line: '%sudo-nopasswd ALL=(ALL:ALL) NOPASSWD:ALL'
regexp: '^%sudo-nopasswd'

Dafür verwende ich das Ansible-Modul lineinfile mit folgenden Parametern:

  • Mit dest gebe ich die Datei an, die bearbeitet werden soll.
  • Mit line übergebe ich die Zeile, die in die Datei eingetragen werden soll.
  • Sollte es eine andere Zeile geben, auf die der mit regexp übergebene reguläre Ausdruck zutrifft (also eine, die mit %sudo-nopasswd beginnt), wird diese gelöscht.

In den letzten zwei Zeilen muss ich die Strings entsprechend YAML-Cookbook in Anführungszeichen setzen, da % ein Sonderzeichen ist.

Mit den restlichen vier Tasks kopiere ich die Public Keys für die User root und sven auf den Server. Der Public Key für den root-User wurde zwar bereits bei der Installation des Betriebssystems kopiert, doch möchte ich dieses Playbook auch nutzen können, um den Key ggf. zu ändern oder evtl. einen Key auf einem Server zu installieren, den ich nicht über das installimage-Skript von Hetzner eingerichtet habe.

Task 7 – .ssh-Verzeichnis für root-User anlegen

- name: Create root user's .ssh directory
file:
path: /root/.ssh
state: directory
owner: root
group: root
mode: 0700

Hier wird mit Hilfe des Ansible-Moduls „file“ das Verzeichnis /root/.ssh/ erstellt und – wie beim copy-Modul – Eigentümer, Gruppe und Modus gesetzt.

Task 8 – Public Keys für root-User kopieren

- name: Copy root user's authorized_keys
copy:
src: authorized_keys_root
dest: /root/.ssh/authorized_keys
owner: root
group: root
mode: 0600

Das copy-Modul habe ich bereits zuvor erklärt. Tasks 9 und 10 erstellen analog das Verzeichnis /home/sven/.ssh/ und kopieren die Quelldatei authorized_keys_sven nach /home/sven/.ssh/authorized_keys. Die Dateien authorized_keys_root und authorized_keys_sven liegen im Quellverzeichnis roles/users/files/ und enthalten die Public Keys derjenigen Personen, die als User root bzw. sven per SSH auf den Server Zugriff haben sollen.

Tasks auf mehrere Dateien verteilen

Die Datei roles/users/tasks/main.yml ist ziemlich lang und unübersichtlich geworden, was mir nicht gefällt. Ich werde daher einzelne Codeblöcke in andere Dateien extrahieren und diese mit import_tasks in die main.yml importieren. Ich teile die Datei in drei logische Blöcke auf:

  1. Installation und Konfiguration des sudo-Kommandos,
  2. Anlegen des Users und Hinzufügen zu/Entfernen aus Gruppen,
  3. Kopieren der Public Keys.

Die main.yml sieht nun wie folgt aus:

---
- import_tasks: setup_sudo.yml
- import_tasks: setup_users.yml
- import_tasks: upload_pubkeys.yml

Die drei neuen Dateien liegen im selben Verzeichnis wie die main.yml, also in roles/users/tasks/ und sehen wie folgt aus:

Datei roles/users/tasks/setup_sudo.yml:

---
- name: Install sudo
apt:
name: sudo

- name: Create "sudo-nopasswd" group
group:
name: sudo-nopasswd

- name: Add "sudo-nopasswd" group to "sudoers" file
lineinfile:
dest: /etc/sudoers
line: '%sudo-nopasswd ALL=(ALL:ALL) NOPASSWD:ALL'
regexp: '^%sudo-nopasswd'

Datei roles/users/tasks/setup_users.yml:

---
- name: Create "sven" user
user:
name: sven
shell: /bin/bash
uid: 1000
group: users
groups: sudo
append: true # --> user is not removed from any other group
password: $6$2i/vlnXuBj/m8A$y9xNcMAJQstggFMSWYbVeJStvvDpk4s4jScPv074Fb94jSfCbxyWSPz.tmMPQ6Qiy6mnpn07SQRTe6Sex4Pfi/

- name: Add user "sven" to "sudo-nopasswd" group
user:
name: sven
groups: sudo-nopasswd
append: true # --> user is not removed from any other group
when: passwordless_sudo is defined and passwordless_sudo == true

- name: Remove user "sven" from "sudo-nopasswd" group
shell: /usr/sbin/delgroup sven sudo-nopasswd
when: not (passwordless_sudo is defined and passwordless_sudo == true)
ignore_errors: yes

Datei roles/users/tasks/upload_pubkeys.yml:

---
- name: Create root user's .ssh directory
file:
path: /root/.ssh
state: directory
owner: root
group: root
mode: 0700

- name: Copy root user's authorized_keys
copy:
src: authorized_keys_root
dest: /root/.ssh/authorized_keys
owner: root
group: root
mode: 0600

- name: Create sven user's .ssh directory
file:
path: /home/sven/.ssh
state: directory
owner: sven
group: users
mode: 0700

- name: Copy sven user's authorized_keys
copy:
src: authorized_keys_sven
dest: /home/sven/.ssh/authorized_keys
owner: sven
group: users
mode: 0600

SSH-Konfiguration optimieren

Um meinen Server besser gegen Angriffe zu schützen, möchte ich gegenüber der Standard-SSH-Konfiguration einige Änderungen vornehmen. Ich bin kein Security-Experte und werde hier daher nicht auf die Details eingehen – dafür gibt es hinreichend Lektüre im Internet. Folgende Einstellungen möchte ich u. a. ändern:

  • Ausschließlich Public-Key-Authentifizierung zulassen mit modernsten Verschlüsselungsalgorithmen;
  • Verbindung als root-User unterbinden;
  • Verbindung über einen anderen als den Standard-Port 22, um für einen Großteil böswilliger Scans unsichtbar zu werden;
  • Agent-Forwarding deaktivieren;
  • TCP-Forwarding nur bei Bedarf aktivieren (per Variable);
  • Login- und Verbindungs-Timeouts minimieren.

Für die Konfiguration lege ich die Rolle ssh an und erstelle die unter diesem Absatz abgedruckte SSH-Konfigurationsdatei. Diese enthält u. a. die IP-Adresse des Servers als Variable. Um diese beim Ausführen des Ansible-Playbooks zu ersetzen, muss die Datei im templates/-Verzeichnis abgelegt werden und nicht im files/-Verzeichnis. Der Pfad der Datei lautet somit roles/ssh/templates/sshd_config.j2. Die Endung „.j2“ ist nicht zwingend, ich nutze sie aber, um in meiner IDE das Jinja2-Syntax-Highlighting zu aktivieren (in IntelliJ IDEA muss dazu das „Python“-Plugin installiert werden). Die Datei sieht wie folgt aus:

AcceptEnv LANG LC_*
AllowAgentForwarding no
{# using a boolean here and not a string, as a string might end up as 'True' if you set the variable to yes (without quote marks) #}
AllowTcpForwarding {{ 'yes' if allow_tcp_forwarding is defined and allow_tcp_forwarding == true else 'no' }}
AllowGroups sshers
ChallengeResponseAuthentication no
Ciphers chacha20-poly1305@openssh.com,aes256-gcm@openssh.com,aes256-ctr
ClientAliveCountMax 3
ClientAliveInterval 15
Compression delayed

GatewayPorts no

HostbasedAuthentication no
IgnoreRhosts yes
KexAlgorithms curve25519-sha256@libssh.org
ListenAddress {{ ansible_default_ipv4.address }}:{{ ssh_port | default('22') }}
LoginGraceTime 10
LogLevel INFO
MaxAuthTries 3
PasswordAuthentication no
PermitEmptyPasswords no
PermitRootLogin no
Protocol 2
PubkeyAuthentication yes
StrictModes yes
Subsystem sftp /usr/lib/openssh/sftp-server
SyslogFacility AUTH
UsePAM yes
UsePrivilegeSeparation yes

X11Forwarding no

Die Erklärung aller Parameter würde den Rahmen dieses Artikels sprengen. Ich verweise dazu auf das sshd_config-Handbuch. Ich beschränke mich auf die zwei Parameter, bei denen ich Variablen verwendet habe:

  • AllowTcpForwarding: dies möchte ich, wie in der Einführung erwähnt, nach Bedarf aktivieren können und führe dafür die Variable allow_tcp_forwarding ein, die ich fürs Erste in meiner Host-Konfigurationsdatei auf false setze.
  • ListenAddress: diese setze ich auf die IP-Adresse des Servers gefolgt von der Port-Nummer, die ich in der Variablen ssh_port definieren kann. Ist die Variable nicht definiert, wird der Standardport 22 verwendet. Ich wähle einen zufälligen Port – Port 5930 und trage dazu in meiner Host-Konfigurationsdatei host_vars/happy1.happycoders.eu folgendes ein: ssh_port: 5930.

Um die SSH-Konfigurationsdatei zu kopieren und den SSH-Service neuzustarten, lege ich die Task-Definitionsdatei roles/ssh/tasks/main.yml an, welche die folgenden drei Tasks enthält:

Task 1 – Gruppe sshers erstellen:

---
- name: Create "sshers" group
group:
name: sshers

Aufgrund des Konfigurationseintrags AllowGroups sshers in der sshd_config dürfen sich nur User dieser Gruppe per SSH verbinden.

Task 2 – User sven zur Gruppe sshers hinzufügen:

- name: Add user "sven" to "sshers" group
user:
name: sven
groups: sshers
append: true # --> user is not removed from any other group

Task 3 – SSH-Konfigurationsdatei kopieren:

- name: Configure SSH server
template:
src: sshd_config.j2
dest: /etc/ssh/sshd_config
owner: root
group: root
mode: 0644
notify:
- Restart SSH service

Hier verwende ich das bis jetzt noch nicht eingesetze Ansible-Modul template, welches eine Template-Datei kopiert und dabei die darin enthaltenen Variablen ersetzt. Die Parameter sind die gleichen wie beim zuvor bereits genutzten copy-Modul.

Neu sind ebenfalls die letzten zwei Zeilen: Diese sorgen dafür, dass im Fall einer Änderung der sshd_config (und nur dann) der Handler Restart SSH service ausgeführt wird. Ein Handler ist ein Task, der als Reaktion auf ein bestimmtes Ereignis ausgeführt wird. Den Handler definiert man im Unterverzeichnis handlers/ der Rolle, wiederum in einer main.yml, also in roles/ssh/handlers/main.yml:

---
- name: Restart SSH service
service:
name: ssh
state: restarted

Ich verwende hier das Ansible-Modul service, um den SSH-Service neuzustarten. Wichtig ist zu wissen, dass der Handler nicht sofort nach dem Task Configure SSH server ausgeführt wird, sondern erst – gemeinsam mit allen anderen durch notify aktivierten Handlern – am Ende des Playbooks. Dies kann mit dem Task meta: flush_handlers auch vorgezogen werden, doch das ist an dieser Stelle nicht erforderlich.

Playbook ausführen

Das Bootstrap-Playbook ist noch nicht ganz fertig (es fehlen noch die Konfiguration der Aliase und die Installation einiger Tools), doch es ist nun an der Zeit es erstmals laufen zu lassen. Dies mache ich mit folgendem Kommando:

ansible-playbook --extra-vars "target=happy1.happycoders.eu" bootstrap.yml
Ausführen des Bootstrap-Ansible-Playbooks
Ausführen des Bootstrap-Ansible-Playbooks

Das Playbook ist zu meiner Zufriedenheit erfolgreich durchgelaufen. Der Fehler beim Entfernen des Users sven aus der Gruppe sudo-nopasswd war erwartet und wird korrekterweise ignoriert. Da das Playbook die SSH-Einstellungen des Servers geändert hat, muss ich mich ab sofort über den neuen Port 5930 und als User sven mit dem Server verbinden:

ssh -p 5930 sven@happy1.happycoders.eu
Login als User "sven"
Login als User „sven“

Bash-Aliase anlegen

An der Standard-Debian-Installation stört mich, dass für den root-User und reguläre User unterschiedliche Bash-Aliase definiert sind. Für den root-User sind in der Datei /root/.bashrc standardmäßig folgende Aliase hinterlegt:

export LS_OPTIONS='--color=auto'
[...]
alias ls='ls $LS_OPTIONS'
alias ll='ls $LS_OPTIONS -l'
alias l='ls $LS_OPTIONS -lA'

Für reguläre User, also den User sven, ist hingegen nur ein einzelner Alias definiert, in der Datei /home/sven/.bashrc – der Rest ist auskommentiert und unterscheidet sich von denen des root-Users:

alias ls='ls --color=auto'
#alias dir='dir --color=auto' #alias vdir='vdir --color=auto'
#alias grep='grep --color=auto'
#alias fgrep='fgrep --color=auto'
#alias egrep='egrep --color=auto'
[...]
#alias ll='ls -l'
#alias la='ls -A'
#alias l='ls -CF'

Ich möchte für den sven-User die ls-Aliase des root-Users übernehmen und zusätzlich die auskommentierten Aliase für grep, fgrep, egrep aktivieren. Ich möchte jedoch nicht die /home/sven/.bashrc überschreiben, da mein Playbook auch mit zukünftigen Debian-Releases, welche evtl. eine andere .bashrc installieren könnten, kompatibel sein soll. Stattdessen werde ich die Aliase in die Datei /home/sven/.bash_aliases schreiben. Dazu erstelle ich eine Rolle aliases und lege darin die Datei roles/aliases/files/.bash_aliases mit folgendem Inhalt an:

# enable color support of ls and also add handy aliases
if [ -x /usr/bin/dircolors ]; then
test -r ~/.dircolors && eval "$(dircolors -b ~/.dircolors)" || eval "$(dircolors -b)"
alias ls='ls --color=auto'
alias grep='grep --color=auto'
alias fgrep='fgrep --color=auto'
alias egrep='egrep --color=auto'
fi

# ls aliases from root user
alias l='ls -lA'
alias ll='ls -l'

Das Kopieren der Datei auf den Server geschieht mit folgender Task-Definition in der Datei roles/aliases/tasks/main.yml:

---
- name: Copy sven's .bash_aliases
copy:
src: .bash_aliases
dest: /home/sven/.bash_aliases
owner: sven
group: users
mode: 0644

Tools installieren

Als letztes installiere ich einige Tools, die ich auf allen Servern benötigen werde. Ich erstelle dazu eine Rolle tools und definiere in der Tasks-Datei roles/tools/tasks/main.yml, welche Packages installiert werden sollen:

- name: Install Midnight Commander, NTP daemon
apt:
name:
- mc
- ntp
cache_valid_time: 3600

Das Modul „apt“ habe ich bereits weiter oben beschrieben. Neu ist der Parameter cache_valid_time: 3600. Damit lege ich fest, dass vor der Installation die Package-Liste mittels apt update aktualisiert werden soll, wenn diese älter als 3.600 Sekunden (1 Stunde) ist.

Playbook erneut ausführen

Ich füge die zwei neuen Rollen aliases und tools in das Bootstrap-Playbook bootstrap.yml ein, welches nun wie folgt aussieht:

---
- hosts: "{{ target }}"
remote_user: root
roles:
- apt_upgrade
- hostnames
- users
- ssh
- aliases
- tools

Ich starte das Playbook erneut:

ansible-playbook --extra-vars "target=happy1.happycoders.eu" bootstrap.yml

Doch dieses Mal bricht Ansible mit folgender Fehlermeldung ab: „Failed to connect to the host via ssh: ssh: connect to host happy1.happycoders.eu port 22: Connection refused“.

"Connection refused" beim erneuten Ausführen des Bootstrap-Playbooks
„Connection refused“ beim erneuten Ausführen des Bootstrap-Playbooks

Der Grund ist ganz einfach: Das Bootstrap-Playbook versucht sich als root-User auf den Standard-SSH-Port 22 zu verbinden. Durch die gehärtete SSH-Konfiguration muss die Verbindung allerdings als User sven auf Port 5930 erfolgen.

Happy Ansible Playbook

Möchte ich die Konfiguration ändern, kann ich das also nicht über das Bootstrap-Playbook erreichen. Nicht nur kann es sich mit der bisherigen Konfiguration nicht mehr mit dem Server verbinden – hinzu kommt, dass es alle Packages aktualisiert, was ich im Produktivbetrieb nicht unkontrolliert geschehen lassen möchte, wann immer ich etwas an der Serverkonfiguration ändere.

Die Lösung ist: Ich lege ein neues Playbook happy1.yml an, welches sich als User sven auf Port 5930 verbindet und alle Rollen ausführt, die auch das Bootstrap-Playbook ausführen würde – mit Ausnahme der Rolle apt_upgrade (aus o. g. Grund):

---
- hosts: happy1.happycoders.eu
vars:
ansible_port: "{{ ssh_port | default('22') }}"
remote_user: sven
become: yes
roles:
- hostnames
- users
- ssh
- aliases
- tools

Folgendes unterscheidet dieses Playbook vom Bootstrap-Playbook:

  • Dieses Playbook ist kein generisches, sondern ein konkretes für meinen Server happy1.happycoders.eu. Somit kann ich diesen über den Parameter hosts direkt angeben und den Parameter --extra-vars "target=happy1.happycoders.eu" beim Ausführen des Playbooks weglassen.
  • Über vars setze ich die Variable ansible_port auf den SSH-Port, den ich in der Host-Konfigurationsdatei in der Variable ssh_port festgelegt habe. Somit verbindet sich Ansible über diesen Port mit dem Server.
  • Durch die Angabe von remote_user: sven verbindet sich Ansible als User sven.
  • Die Angabe become: yes führt dazu, dass Ansible alle Kommandos auf dem Server als root-User ausführt.

Beim Ausführen dieses Playbooks muss ich nun zusätzlich den Parameter -K angeben, damit Ansible mich nach dem sudo-Passwort fragt:

ansible-playbook -K happy1.yml
Ausführen des Ansible-Playbooks "happy1.yml"
Ausführen des Ansible-Playbooks „happy1.yml“

Ihr seht, dass das Playbook komplett durchläuft und dabei bei fast allen Tasks „ok“ meldet, was bedeutet, dass keine Änderungen durchgeführt wurden. Lediglich für die zwei neuen Tasks meldet Ansible „changed“, was bedeutet: Die Bash-Aliase sowie die Tools wurden erfolgreich installiert.

sudo ohne Passwort

Wie ihr euch vielleicht erinnert, habe ich im Bootstrap-Playbook in der users-Rolle die Möglichkeit geschaffen, während der Entwicklungsphase auf die Eingabe des Passworts für sudo verzichten zu können. Dies möchte ich nun aktivieren. Dazu muss ich in die Host-Konfigurationsdatei host_vars/happy1.happycoders.eu die Zeile passwordless_sudo: true eintragen und das Playbook noch einmal ausführen:

ansible-playbook -K happy1.yml
User "sven" wird zur Gruppe "sudo-nopasswd" hinzugefügt
User „sven“ wird zur Gruppe „sudo-nopasswd“ hinzugefügt

Für den Task Add user "sven" to "sudo-nopasswd" group meldet Ansible „changed“, was bedeutet, dass der Task ausgeführt, also der User zu der Gruppe hinzugefügt wurde. Und den Task Remove user "sven" from "sudo-nopasswd" group quittiert Ansible mit „skipping“, d. h. dieser Schritt wurde übersprungen, der User also nicht wieder aus der Gruppe entfernt. Würde ich nun passwordless_sudo auf false setzen und das Playbook erneut ausführen, würde der erste Task übersprungen und der zweite ausgeführt werden.

Bei den zukünftigen Ausführungen des Playbooks kann der Parameter -K nun weggelassen werden (so lange bis ich passwordless_sudo wieder auf false setze):

ansible-playbook happy1.yml

Firewall aus der Ansible Galaxy

Als letzten Schritt in diesem Artikel möchte ich nun noch eine Firewall installieren, die nur Anfragen auf den Ports 80, 443 und 5930 hineinlässt. Da das recht komplex werden könnte, schaue ich, ob sich nicht schon jemand anders die Mühe gemacht und eine Firewall-Rolle geschrieben hat. Die Ansible-Entwickler-Community veröffentlicht Rollen auf Ansible Galaxy. Dort suche ich nach dem Stichwort „Firewall“:

Suche nach "Firewall" in der Ansible Galaxy
Suche nach „Firewall“ in der Ansible Galaxy

Ich suche auch noch einmal bei Google und finde einige GitHub-Projekte, wovon jedoch nur die eben schon in der Ansible Galaxy gefundene Firewall-Rolle von geerlingguy mit mehr als 200 Sternen Relevanz hat; alle übrigen Ansible-Firewall-Projekte haben weniger als fünf Sterne.

Der Entwickler schreibt über seine Rolle: „This firewall aims for simplicity over complexity“, was sich im weiteren Verlauf bestätigt: Ich kann einzelne Ports freischalten, und wenn ich mehr in die Details gehen will, beliebig komplexe iptables-Kommandos hinzufügen.

Installiert wird die Rolle auf dem lokalen Rechner ganz einfach wie folgt:

ansible-galaxy install geerlingguy.firewall
Installation der Rolle "geerlingguy.firewall" aus der Ansible Galaxy
Installation der Rolle „geerlingguy.firewall“ aus der Ansible Galaxy

Ich füge die Rolle geerlingguy.firewall in das Playbook happy1.yml ein:

---
- hosts: happy1.happycoders.eu
vars:
ansible_port: "{{ ssh_port | default('22') }}"
remote_user: sven
become: yes
roles:
- hostnames
- users
- ssh
- aliases
- tools
- geerlingguy.firewall

Konfiguriert wird die Rolle über Variablen, die ich in die Host-Konfigurationsdatei host_vars/happy1.happycoders.eu einfüge. Ich muss hier lediglich die drei Ports angeben, die ich geöffnet haben möchte, den Rest erledigt die Rolle. Folgendes füge ich in die Host-Konfigurationsdatei ein:

firewall_allowed_tcp_ports:
- 80
- 443
- 5930

Ich führe das Playbook aus:

ansible-playbook happy1.yml
Konfiguration der Firewall mit Ansible
Konfiguration der Firewall mit Ansible

Nach Anwendung des Playbooks prüfe ich, ob die Firewall-Settings aktiv sind:

ssh -p 5930 sven@happy1.happycoders.eu
sudo iptables -L
sudo ip6tables -L
Anzeige der Firewall-Konfiguration mit "iptables -L" und "ip6tables -L"
Anzeige der Firewall-Konfiguration mit „iptables -L“ und „ip6tables -L“

Ihr seht: Die Firewall wurde wie gewünscht konfiguriert.

Zusammenfassung und Ausblick

In diesem Teil der Serie habe ich euch das „Bootstrapping“ des Servers gezeigt, also all diejenigen Installationen und Konfigurationen, die ich auch für zukünftige Server mit möglicherweise anderen Anwendungsbereichen benötigen werde.

Im dritten Teil der Serie werde ich dann das happy1-Playbook erweitern und euch zeigen, wie ich mit Ansible Docker, MySQL und WordPress installiere.

Ich bedanke mich bei euch für das Lesen dieses Artikels und würde ich mich freuen, wenn ihr einen Kommentar hinterlasst und mir mitteilt, wie euch das Tutorial gefallen habt, was ich hätte besser machen können und auch was für Themen euch generell interessieren. Vielen Dank!

Kommentar verfassen

Deine E-Mail-Adresse wird nicht veröffentlicht. Erforderliche Felder sind mit * markiert.