second commit

This commit is contained in:
2026-08-19 22:33:19 +02:00
parent 411812e954
commit 199f306993
107 changed files with 5984 additions and 0 deletions

View File

@ -0,0 +1,35 @@
---
# Gemeinsame Variablen. Umgebungsspezifische Werte in
# jumphost_with_nginx.yml / jumphost_no_nginx.yml ueberschreiben
# (oder direkt hier anpassen, wenn nur eine Umgebung existiert).
jumphost_app_user: jumphost
jumphost_app_group: jumphost
jumphost_home: /opt/jumphost
jumphost_data_dir: /var/lib/jumphost
jumphost_venv: "{{ jumphost_home }}/venv"
jumphost_repo_src: "{{ playbook_dir }}/../.." # Projekt-Root (enthaelt app/, static/, templates/)
jumphost_listen_uds: /run/jumphost/app.sock
jumphost_app_port: 8443 # nur relevant wenn enable_nginx_proxy: false
guacd_port: 4822
# --- nginx / TLS ------------------------------------------------------------
enable_nginx_proxy: true
tls_mode: internal_pki # internal_pki | external_reverse_proxy | acme_public
# Nur fuer tls_mode: external_reverse_proxy relevant:
external_reverse_proxy_cidr: "10.10.5.0/24"
internal_hop_plaintext_accepted: false # bewusste Ausnahme, siehe Konzept 7.2a
# Nur fuer tls_mode: acme_public relevant:
acme_domain: "jumphost.example.com"
acme_email: "admin@example.com"
# --- Firewall ----------------------------------------------------------------
ssh_admin_access_cidr: "10.10.1.0/24" # Management-Netz fuer SSH-Zugriff AUF den Jumphost selbst
target_networks: # Netze der Zielsysteme (fuer ausgehende Regeln)
- "10.20.0.0/16"
# --- Backup --------------------------------------------------------------------
backup_dir: /var/backups/jumphost
backup_retention_days: 30

View File

@ -0,0 +1,5 @@
---
# Beispiel-Override fuer eine Umgebung OHNE nginx (siehe Konzept 7.2/9):
# Die Python-App terminiert TLS direkt.
enable_nginx_proxy: false
jumphost_app_port: 8443

View File

@ -0,0 +1,4 @@
---
# Beispiel-Override fuer eine Umgebung MIT nginx (siehe Konzept 7.2/9).
enable_nginx_proxy: true
tls_mode: internal_pki

View File

@ -0,0 +1,6 @@
---
# Mit `ansible-vault encrypt group_vars/vault.yml` verschluesseln, NIEMALS
# im Klartext committen (siehe Konzept 6.4/7.3).
vault_jumphost_kek: "CHANGE_ME_32_BYTES_HEX_0123456789abcdef0123456789abcdef"
vault_jumphost_session_secret: "CHANGE_ME_32_BYTES_HEX_0123456789abcdef0123456789abcdef"
vault_jumphost_initial_admin_password: "CHANGE_ME_STRONG_PASSWORD"

View File

@ -0,0 +1,6 @@
; Beispiel-Inventory. Kopieren nach production.ini und anpassen.
[jumphosts]
jumphost01.example.internal ansible_user=deploy
[jumphosts:vars]
ansible_python_interpreter=/usr/bin/python3

View File

@ -0,0 +1,4 @@
---
- name: reload systemd
ansible.builtin.systemd:
daemon_reload: true

View File

@ -0,0 +1,47 @@
---
- name: Backup-Verzeichnis anlegen
ansible.builtin.file:
path: "{{ backup_dir }}"
state: directory
owner: "{{ jumphost_app_user }}"
group: "{{ jumphost_app_group }}"
mode: "0700"
- name: age installieren (Backup-Verschluesselung)
ansible.builtin.apt:
name: age
state: present
update_cache: true
- name: Backup-Skript ausrollen
ansible.builtin.template:
src: backup.sh.j2
dest: "{{ jumphost_home }}/scripts/backup.sh"
owner: "{{ jumphost_app_user }}"
group: "{{ jumphost_app_group }}"
mode: "0750"
- name: systemd-Service fuer Backup ausrollen
ansible.builtin.template:
src: jumphost-backup.service.j2
dest: /etc/systemd/system/jumphost-backup.service
owner: root
group: root
mode: "0644"
notify: reload systemd
- name: systemd-Timer fuer Backup ausrollen
ansible.builtin.template:
src: jumphost-backup.timer.j2
dest: /etc/systemd/system/jumphost-backup.timer
owner: root
group: root
mode: "0644"
notify: reload systemd
- name: Backup-Timer aktivieren
ansible.builtin.systemd:
name: jumphost-backup.timer
daemon_reload: true
enabled: true
state: started

View File

@ -0,0 +1,17 @@
#!/usr/bin/env bash
# Konsistentes, verschluesseltes SQLite-Backup (Konzept 6.8).
# KEK-Backup erfolgt bewusst GETRENNT (siehe Konzept 6.8) -- dieses Skript
# sichert ausschliesslich die Datenbank, kein Schluesselmaterial.
set -euo pipefail
DB_PATH="{{ jumphost_data_dir }}/jumphost.db"
BACKUP_DIR="{{ backup_dir }}"
TS="$(date -u +%Y%m%dT%H%M%SZ)"
OUT_PLAIN="${BACKUP_DIR}/jumphost_${TS}.db"
OUT_ENC="${OUT_PLAIN}.age"
sqlite3 "$DB_PATH" "VACUUM INTO '${OUT_PLAIN}'"
age -r "{{ backup_age_public_key | default('AGE_PUBLIC_KEY_PLACEHOLDER') }}" -o "${OUT_ENC}" "${OUT_PLAIN}"
shred -u "${OUT_PLAIN}"
find "$BACKUP_DIR" -name 'jumphost_*.db.age' -mtime +{{ backup_retention_days }} -delete

View File

@ -0,0 +1,16 @@
[Unit]
Description=Jumphost verschluesseltes DB-Backup
[Service]
Type=oneshot
User={{ jumphost_app_user }}
Group={{ jumphost_app_group }}
ExecStart=/usr/bin/env bash {{ jumphost_home }}/scripts/backup.sh
NoNewPrivileges=true
ProtectSystem=strict
ProtectHome=true
PrivateTmp=true
ReadWritePaths={{ backup_dir }} {{ jumphost_data_dir }}
CapabilityBoundingSet=
UMask=0077

View File

@ -0,0 +1,10 @@
[Unit]
Description=Taeglicher Jumphost-Backup-Timer
[Timer]
OnCalendar=daily
RandomizedDelaySec=1800
Persistent=true
[Install]
WantedBy=timers.target

View File

@ -0,0 +1,5 @@
---
- name: restart fail2ban
ansible.builtin.service:
name: fail2ban
state: restarted

View File

@ -0,0 +1,38 @@
---
# Zweite Verteidigungslinie gegen Brute-Force zusaetzlich zum
# Anwendungs-Rate-Limiting (Konzept 6.2). Ueberwacht die Uvicorn-/nginx-
# Access-Logs auf gehaeufte 401-Antworten vom Login-Endpunkt.
- name: fail2ban installieren
ansible.builtin.apt:
name: fail2ban
state: present
update_cache: true
- name: Filter fuer Jumphost-Login-Fehlversuche
ansible.builtin.copy:
dest: /etc/fail2ban/filter.d/jumphost-login.conf
owner: root
group: root
mode: "0644"
content: |
[Definition]
failregex = ^.*"POST /auth/login(/totp)? HTTP/.*" 401 .*client_ip=<HOST>.*$
^.*"POST /auth/login(/totp)? HTTP/.*" 401 .*<HOST>.*$
ignoreregex =
- name: Jail fuer Jumphost-Login aktivieren
ansible.builtin.copy:
dest: /etc/fail2ban/jail.d/jumphost.conf
owner: root
group: root
mode: "0644"
content: |
[jumphost-login]
enabled = true
filter = jumphost-login
logpath = /var/log/jumphost/access.log
maxretry = 8
findtime = 300
bantime = 1800
notify: restart fail2ban

View File

@ -0,0 +1,5 @@
---
- name: reload nftables
ansible.builtin.service:
name: nftables
state: restarted

View File

@ -0,0 +1,25 @@
---
# Default-Deny-Firewall (Konzept 6.7). Eingehend nur admin-SSH (aus dem
# Management-Netz) und der oeffentliche App-/nginx-Port; ausgehend nur zu den
# definierten Zielsystem-Netzen sowie DNS/NTP.
- name: nftables installieren
ansible.builtin.apt:
name: nftables
state: present
update_cache: true
- name: nftables-Regelsatz ausrollen
ansible.builtin.template:
src: jumphost.nft.j2
dest: /etc/nftables.conf
owner: root
group: root
mode: "0640"
notify: reload nftables
- name: nftables aktivieren und starten
ansible.builtin.service:
name: nftables
state: started
enabled: true

View File

@ -0,0 +1,46 @@
#!/usr/sbin/nft -f
flush ruleset
table inet filter {
chain input {
type filter hook input priority 0; policy drop;
iif lo accept
ct state established,related accept
ct state invalid drop
icmp type echo-request limit rate 5/second accept
ip6 nexthdr icmpv6 icmpv6 type echo-request limit rate 5/second accept
tcp dport 22 ip saddr {{ ssh_admin_access_cidr }} accept
{% if enable_nginx_proxy %}
tcp dport 443 accept
{% else %}
tcp dport {{ jumphost_app_port }} accept
{% endif %}
}
chain forward {
type filter hook forward priority 0; policy drop;
}
chain output {
type filter hook output priority 0; policy drop;
oif lo accept
ct state established,related accept
udp dport 53 accept
tcp dport 53 accept
udp dport 123 accept
{% for net in target_networks %}
ip daddr {{ net }} accept
{% endfor %}
# ACME/interne PKI (tls_certificates-Rolle) und OS-Paketquellen
tcp dport 443 accept
tcp dport 80 accept
}
}

View File

@ -0,0 +1,25 @@
---
# Vendored Frontend-Assets (xterm.js, guacamole-common-js). Bewusst OHNE
# Laufzeit-CDN-Bezug (Konzept 4.1/6.6) -- der Build laeuft hier einmalig auf
# dem Zielsystem (oder alternativ auf einem Build-Host, siehe Variante unten).
- name: Node.js/npm fuer den Asset-Build installieren
ansible.builtin.apt:
name: npm
state: present
update_cache: true
- name: Frontend-Assets bauen (scripts/fetch_frontend_assets.sh)
ansible.builtin.command:
cmd: "bash {{ jumphost_repo_src }}/scripts/fetch_frontend_assets.sh"
delegate_to: localhost
become: false
run_once: true
register: _asset_build
changed_when: "'Fertig' in _asset_build.stdout"
# Hinweis: In restriktiveren Umgebungen (Jumphost selbst ohne Internetzugang
# fuer npm) diesen Task durch eine Kopie von vorgefertigten, im internen
# Artefakt-Repository abgelegten vendor/-Dateien ersetzen -- die Zielstruktur
# (static/js/vendor/*.js) bleibt identisch, siehe python_runtime-Rolle, die
# static/ anschliessend synchronisiert.

View File

@ -0,0 +1,10 @@
---
- name: reload systemd
ansible.builtin.systemd:
daemon_reload: true
- name: restart guacd
ansible.builtin.service:
name: guacd
state: restarted
enabled: true

View File

@ -0,0 +1,64 @@
---
# guacd + FreeRDP als reine Protokoll-Engine fuer RDP (Konzept 3/4.3).
# Laeuft ausschliesslich lokal gebunden, kein Netzwerkzugriff von aussen.
- name: guacd und FreeRDP-Plugin installieren
ansible.builtin.apt:
name:
- guacd
- libguac-client-rdp0
state: present
update_cache: true
- name: Dedizierten guacd-User sicherstellen (Paket legt i.d.R. bereits einen an)
ansible.builtin.user:
name: guacd
system: true
shell: /usr/sbin/nologin
create_home: false
ignore_errors: true
- name: guacd nur an localhost binden
ansible.builtin.lineinfile:
path: /etc/guacamole/guacd.conf
regexp: '^bind_host'
line: "bind_host = 127.0.0.1"
create: true
notify: restart guacd
- name: guacd-Port setzen
ansible.builtin.lineinfile:
path: /etc/guacamole/guacd.conf
regexp: '^bind_port'
line: "bind_port = {{ guacd_port }}"
create: true
notify: restart guacd
- name: RDP-Laufwerksumleitungs-Verzeichnisse anlegen (Filetransfer, Konzept 4.3)
ansible.builtin.file:
path: "/var/lib/jumphost/rdp-drives"
state: directory
owner: guacd
group: guacd
mode: "0750"
- name: Override-Verzeichnis fuer guacd-Unit anlegen
ansible.builtin.file:
path: /etc/systemd/system/guacd.service.d
state: directory
owner: root
group: root
mode: "0755"
- name: Gehaertete systemd-Unit fuer guacd ausrollen (Override)
ansible.builtin.template:
src: guacd.service.j2
dest: /etc/systemd/system/guacd.service.d/override.conf
owner: root
group: root
mode: "0644"
vars:
_dummy: true
notify:
- reload systemd
- restart guacd

View File

@ -0,0 +1,20 @@
; Hardening-Override fuer die vom Debian/Ubuntu-Paket mitgelieferte
; guacd.service (Konzept 8.2). Nur lokale Kommunikation zur App (Unix-Domain
; oder localhost) sowie ausgehend RDP zu den Zielsystemen.
[Service]
NoNewPrivileges=true
ProtectSystem=strict
ProtectHome=true
PrivateTmp=true
PrivateDevices=true
ProtectKernelTunables=true
ProtectKernelModules=true
ProtectControlGroups=true
RestrictNamespaces=true
RestrictAddressFamilies=AF_UNIX AF_INET AF_INET6
RestrictSUIDSGID=true
MemoryDenyWriteExecute=true
LockPersonality=true
CapabilityBoundingSet=
ReadWritePaths=/var/lib/jumphost/rdp-drives
UMask=0077

View File

@ -0,0 +1,9 @@
---
- name: reload systemd
ansible.builtin.systemd:
daemon_reload: true
- name: restart jumphost-app service
ansible.builtin.service:
name: jumphost-app
state: restarted

View File

@ -0,0 +1,76 @@
---
# Deployt die eigentliche Jumphost-Anwendung als gehaertete systemd-Unit.
# KEK und Session-Secret werden ueber systemd-creds verschluesselt abgelegt
# (Konzept 6.4) -- niemals als Klartext-Env-Variable im Unit-File.
- name: Konfigurationsverzeichnis anlegen
ansible.builtin.file:
path: /etc/jumphost
state: directory
owner: root
group: "{{ jumphost_app_group }}"
mode: "0750"
- name: Pruefen ob systemd-creds verfuegbar ist (systemd >= 250 empfohlen)
ansible.builtin.command: systemd-creds --version
register: _creds_check
changed_when: false
failed_when: false
- name: Warnung ausgeben, falls systemd-creds fehlt
ansible.builtin.debug:
msg: >
WARNUNG: systemd-creds nicht verfuegbar. Fallback auf Env-Variablen
(JUMPHOST_KEK/JUMPHOST_SESSION_SECRET) in einer 0600-EnvironmentFile --
weniger sicher als LoadCredentialEncrypted=, siehe Konzept 6.4.
when: _creds_check.rc != 0
- name: KEK verschluesselt ablegen (systemd-creds)
ansible.builtin.shell: |
set -o pipefail
echo -n '{{ vault_jumphost_kek }}' | systemd-creds encrypt --name=jumphost_kek - /etc/jumphost/jumphost_kek.cred
args:
creates: /etc/jumphost/jumphost_kek.cred
executable: /bin/bash
when: _creds_check.rc == 0
no_log: true
- name: Session-Secret verschluesselt ablegen (systemd-creds)
ansible.builtin.shell: |
set -o pipefail
echo -n '{{ vault_jumphost_session_secret }}' | systemd-creds encrypt --name=jumphost_session_secret - /etc/jumphost/jumphost_session_secret.cred
args:
creates: /etc/jumphost/jumphost_session_secret.cred
executable: /bin/bash
when: _creds_check.rc == 0
no_log: true
- name: Fallback-EnvironmentFile (nur falls systemd-creds fehlt)
ansible.builtin.copy:
dest: /etc/jumphost/env
owner: root
group: "{{ jumphost_app_group }}"
mode: "0640"
content: |
JUMPHOST_KEK={{ vault_jumphost_kek }}
JUMPHOST_SESSION_SECRET={{ vault_jumphost_session_secret }}
when: _creds_check.rc != 0
no_log: true
- name: systemd-Unit fuer die Jumphost-App ausrollen
ansible.builtin.template:
src: jumphost-app.service.j2
dest: /etc/systemd/system/jumphost-app.service
owner: root
group: root
mode: "0644"
notify:
- reload systemd
- restart jumphost-app service
- name: Jumphost-App aktivieren und starten
ansible.builtin.systemd:
name: jumphost-app
daemon_reload: true
enabled: true
state: started

View File

@ -0,0 +1,58 @@
[Unit]
Description=Jumphost Gateway Application
After=network.target guacd.service
Wants=guacd.service
[Service]
Type=simple
User={{ jumphost_app_user }}
Group={{ jumphost_app_group }}
WorkingDirectory={{ jumphost_home }}
Environment=JUMPHOST_ENV=production
Environment=JUMPHOST_DATA_DIR={{ jumphost_data_dir }}
Environment=JUMPHOST_LISTEN_UDS={{ jumphost_listen_uds }}
Environment=JUMPHOST_GUACD_HOST=127.0.0.1
Environment=JUMPHOST_GUACD_PORT={{ guacd_port }}
{% if not enable_nginx_proxy %}
Environment=JUMPHOST_DIRECT_TLS=1
{% endif %}
{% if _creds_check.rc == 0 %}
LoadCredentialEncrypted=jumphost_kek:/etc/jumphost/jumphost_kek.cred
LoadCredentialEncrypted=jumphost_session_secret:/etc/jumphost/jumphost_session_secret.cred
{% else %}
EnvironmentFile=/etc/jumphost/env
{% endif %}
{% if enable_nginx_proxy %}
ExecStart={{ jumphost_venv }}/bin/uvicorn app.main:app --uds {{ jumphost_listen_uds }}
{% else %}
ExecStart={{ jumphost_venv }}/bin/uvicorn app.main:app --host 0.0.0.0 --port {{ jumphost_app_port }} \
--ssl-certfile /etc/jumphost/tls/server.crt --ssl-keyfile /etc/jumphost/tls/server.key
{% endif %}
Restart=on-failure
RestartSec=5
# --- Hardening (Konzept 6.7/8.1) ---------------------------------------------
NoNewPrivileges=true
ProtectSystem=strict
ProtectHome=true
PrivateTmp=true
PrivateDevices=true
ProtectKernelTunables=true
ProtectKernelModules=true
ProtectControlGroups=true
RestrictNamespaces=true
RestrictAddressFamilies=AF_UNIX AF_INET AF_INET6
RestrictSUIDSGID=true
MemoryDenyWriteExecute=true
LockPersonality=true
SystemCallFilter=@system-service
SystemCallErrorNumber=EPERM
CapabilityBoundingSet=
ReadWritePaths={{ jumphost_data_dir }} /run/jumphost /var/log/jumphost
UMask=0077
[Install]
WantedBy=multi-user.target

View File

@ -0,0 +1,5 @@
---
- name: reload nginx
ansible.builtin.service:
name: nginx
state: reloaded

View File

@ -0,0 +1,45 @@
---
# nginx als vorgelagerter Reverse Proxy (Konzept 7.2/7.2a/9). Nur inkludiert
# wenn enable_nginx_proxy: true.
- name: nginx installieren
ansible.builtin.apt:
name: nginx
state: present
update_cache: true
- name: Default-vHost entfernen
ansible.builtin.file:
path: /etc/nginx/sites-enabled/default
state: absent
- name: Proxy-Header-Snippet ausrollen (import)
ansible.builtin.import_tasks: snippets.yml
- name: Jumphost-vHost-Konfiguration ausrollen
ansible.builtin.template:
src: jumphost.conf.j2
dest: /etc/nginx/sites-available/jumphost.conf
owner: root
group: root
mode: "0644"
notify: reload nginx
- name: vHost aktivieren
ansible.builtin.file:
src: /etc/nginx/sites-available/jumphost.conf
dest: /etc/nginx/sites-enabled/jumphost.conf
state: link
notify: reload nginx
- name: Zugriff des nginx-Users auf den App-Unix-Socket sicherstellen
ansible.builtin.user:
name: www-data
groups: "{{ jumphost_app_group }}"
append: true
- name: nginx aktivieren und starten
ansible.builtin.service:
name: nginx
state: started
enabled: true

View File

@ -0,0 +1,20 @@
---
# Wird von tasks/main.yml importiert (siehe include_tasks unten ergaenzen,
# falls Snippet-Datei separat gepflegt werden soll). Der Einfachheit halber
# hier als eigenstaendiger Task-Block gehalten.
- name: Snippet-Verzeichnis anlegen
ansible.builtin.file:
path: /etc/nginx/snippets
state: directory
owner: root
group: root
mode: "0755"
- name: Proxy-Header-Snippet ausrollen
ansible.builtin.template:
src: jumphost_proxy_headers.conf.j2
dest: /etc/nginx/snippets/jumphost_proxy_headers.conf
owner: root
group: root
mode: "0644"
notify: reload nginx

View File

@ -0,0 +1,63 @@
# Jumphost Reverse-Proxy-Konfiguration (Konzept 6.6/7.2a/9).
# tls_mode = {{ tls_mode }}
{% if tls_mode == 'external_reverse_proxy' %}
# Nur der vorgelagerte externe Reverse Proxy darf X-Forwarded-For/-Proto
# setzen -- sonst waere das Audit-Log-Feld client_ip faelschbar (Konzept 7.2a).
set_real_ip_from {{ external_reverse_proxy_cidr }};
real_ip_header X-Forwarded-For;
real_ip_recursive on;
{% endif %}
limit_req_zone $binary_remote_addr zone=jumphost_login:10m rate=10r/m;
server {
{% if tls_mode == 'external_reverse_proxy' and internal_hop_plaintext_accepted %}
# Bewusste, dokumentierte Ausnahme (Konzept 7.2a): Klartext-HTTP auf der
# Strecke externer Proxy -> Jumphost, nur zulaessig in einem eigenen,
# zugriffskontrollierten Netzsegment.
listen 443;
{% else %}
listen 443 ssl;
ssl_certificate /etc/jumphost/tls/server.crt;
ssl_certificate_key /etc/jumphost/tls/server.key;
ssl_protocols TLSv1.3;
ssl_session_cache shared:jumphost_ssl:10m;
{% endif %}
server_name {{ acme_domain | default('_') }};
add_header Strict-Transport-Security "max-age=63072000; includeSubDomains" always;
add_header X-Frame-Options "DENY" always;
add_header X-Content-Type-Options "nosniff" always;
add_header Referrer-Policy "no-referrer" always;
client_max_body_size 210m; # etwas ueber MAX_UPLOAD_BYTES der App (Konzept 6.6)
location /auth/login {
limit_req zone=jumphost_login burst=5 nodelay;
proxy_pass http://unix:{{ jumphost_listen_uds }}:;
include /etc/nginx/snippets/jumphost_proxy_headers.conf;
}
location /ws/ {
proxy_pass http://unix:{{ jumphost_listen_uds }}:;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
proxy_read_timeout 3600s;
include /etc/nginx/snippets/jumphost_proxy_headers.conf;
}
location / {
proxy_pass http://unix:{{ jumphost_listen_uds }}:;
include /etc/nginx/snippets/jumphost_proxy_headers.conf;
}
}
server {
listen 80;
server_name {{ acme_domain | default('_') }};
location /.well-known/acme-challenge/ { root /var/www/html; }
location / { return 301 https://$host$request_uri; }
}

View File

@ -0,0 +1,11 @@
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
{% if tls_mode == 'external_reverse_proxy' %}
# Vom externen Proxy gesetzte Header werden dank set_real_ip_from oben
# vertrauensvoll uebernommen statt hier ueberschrieben zu werden.
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $http_x_forwarded_proto;
{% else %}
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto https;
{% endif %}

View File

@ -0,0 +1,71 @@
# CIS/STIG-Mapping der `os_hardening`-Rolle
Diese Rolle deckt eine bewusst ausgewählte, für einen dedizierten Jumphost
(Debian/Ubuntu-Familie) relevante Teilmenge der CIS Debian/Ubuntu Linux
Benchmarks sowie einzelner DISA-STIG-typischer Controls ab. Sie ist **kein**
vollständiger, zertifizierter Benchmark-Lauf — dafür fehlt ein automatisiertes
Compliance-Scanning-Tool. Empfehlung: nach dem Rollout zusätzlich mit
`oscap xccdf eval` (OpenSCAP) gegen das jeweils zutreffende Profil prüfen und
diese Tabelle bei Abweichungen als Startpunkt für die Nacharbeit nutzen.
Referenz-Control-IDs folgen der CIS-Nummerierung *sinngemäß* (Benchmark-Version
kann je nach Debian/Ubuntu-Release leicht abweichen); wo passend ist zusätzlich
vermerkt, wenn ein Control STIG-typisch, aber nicht CIS-nummeriert ist.
| Bereich | CIS-Control (sinngemäß) | Umsetzung | Status |
|---|---|---|---|
| Unsichere Legacy-Dienste | 2.1.x | `tasks/packages.yml` | Implementiert |
| Automatische Sicherheitsupdates | 1.9 | `tasks/packages.yml` | Implementiert |
| Deaktivierung seltener Dateisystemtreiber | 1.1.1.11.1.1.8 | `tasks/kernel_modules.yml` | Implementiert |
| Deaktivierung seltener Netzwerkprotokolle | 3.4.13.4.4 | `tasks/kernel_modules.yml` | Implementiert |
| USB-Speichermedien deaktivieren | 1.1.23 | `tasks/kernel_modules.yml` | Implementiert |
| Netzwerk-/Kernel-sysctl-Härtung | 3.x, 1.5.x | `tasks/sysctl.yml` | Implementiert |
| Core Dumps deaktivieren | 1.5.1 | `tasks/sysctl.yml` | Implementiert |
| `/tmp`, `/dev/shm` mit noexec/nosuid/nodev | 1.1.2.x | `tasks/mounts.yml` | **Best-Effort** — nur wirksam, wenn bereits eigene Mountpoints existieren (siehe unten) |
| Passwortqualität (Länge/Komplexität) | 5.4.1 | `tasks/pam_password_policy.yml` | Implementiert (lokale OS-Konten) |
| Account-Lockout (pam_faillock) | 5.3.1 | `tasks/pam_password_policy.yml` | Implementiert, siehe Einschränkung unten |
| Passwort-Historie | 5.4.2 | `tasks/pam_password_policy.yml` | Implementiert |
| Passwort-Ablauf (`login.defs`) | 5.4.1.15.4.1.4 | `tasks/pam_password_policy.yml` | Implementiert |
| Erweiterte auditd-Regeln (Identität, Zeit, Netzwerk, Logins, Kernelmodule) | 4.1.34.1.14 | `tasks/auditd.yml` | Implementiert |
| auditd-Log-Handling (keep_logs, space_left_action) | 4.1.2.3, 4.1.2.4 | `tasks/auditd.yml` | Implementiert |
| auditd unveränderlich (`-e 2`) | 4.1.1.4 (STIG) | `tasks/auditd.yml` | Implementiert, **standardmäßig deaktiviert** (Opt-in `os_hardening_auditd_immutable`, da Reboot zum Ändern nötig) |
| Datei-Integritäts-Monitoring (AIDE) | 1.4.1, 1.4.2 | `tasks/aide.yml` | Implementiert |
| Rootkit-Scanner (rkhunter, ergänzend) | STIG-typisch, nicht CIS-nummeriert | `tasks/rootkit_scan.yml` | Implementiert |
| Anmelde-Banner | STIG-typisch (Legal Notice) | `tasks/banners.yml`, `tasks/sshd.yml` | Implementiert |
| cron/at auf autorisierte Nutzer beschränken | 2.4.1.x | `tasks/cron_at.yml` | Implementiert |
| Berechtigungen kritischer Dateien (`/etc/shadow` etc.) | 6.1.x | `tasks/file_permissions.yml` | Implementiert |
| sudo-Logging (`log_input`, `log_output`, `use_pty`) | STIG-typisch | `tasks/file_permissions.yml` | Implementiert |
| `su` auf Gruppe `sudo` beschränken | 5.6 | `tasks/file_permissions.yml` | Implementiert |
| SSH-Daemon-Härtung (Ciphers/KEX/MACs, kein Root-Login, etc.) | 5.2.x | `tasks/sshd.yml` | Implementiert |
| IPv6 deaktivieren | 3.1.1 | `tasks/sysctl.yml` | Implementiert, **Opt-in** (`os_hardening_disable_ipv6`, Default aus) |
## Bewusst nicht automatisiert (mit Begründung)
| Bereich | CIS-Control (sinngemäß) | Warum nicht automatisiert |
|---|---|---|
| Getrenntes Partitionslayout (`/tmp`, `/var`, `/var/log`, `/var/log/audit`, `/home` als eigene Partitionen) | 1.1.11.1.1.30 | Erfordert eine Neupartitionierung der Festplatte — nur zum Zeitpunkt der OS-Installation sinnvoll setzbar, nicht nachträglich per Ansible auf ein laufendes System ohne Datenverlustrisiko. **Empfehlung:** beim Erstellen des Basis-Images/der VM-Vorlage bereits mit diesem Layout provisionieren. |
| Bootloader-Passwort (GRUB) | 1.4.1 (ältere CIS-Nummerierung) | Erfordert physischen/Konsolen-Zugriff zum Testen und ist bei Cloud-/Hypervisor-Images oft irrelevant oder sogar kontraproduktiv (verhindert automatisierten Neustart). Bewusst dem jeweiligen Betreiber überlassen. |
| Volltext-Festplattenverschlüsselung | Nicht CIS-nummeriert, STIG-typisch | Muss bei der OS-Installation eingerichtet werden (LUKS o.ä.), nicht nachträglich per Ansible. In Cloud-Umgebungen häufig durch Provider-seitige Verschlüsselung (z.B. verschlüsselte Volumes) abgedeckt — separat prüfen. |
| Physische Sicherheit / BIOS-UEFI-Passwort | Nicht CIS-nummeriert | Außerhalb der Reichweite von Ansible; organisatorische/physische Maßnahme. |
| Zentrales Log-Forwarding an SIEM/Syslog-Server | Nicht CIS-nummeriert, aber im Jumphost-Konzept Kap. 6.9 gefordert | Erfordert Kenntnis der Ziel-SIEM-Infrastruktur (Empfänger-Host, Protokoll, TLS-Zertifikate) — als eigener Konfigurationspunkt vorgesehen, aber nicht Teil dieser Rolle (siehe Konzept Kap. 6.9, Erweiterungspunkt). |
| Netzwerksegmentierung / Firewalling *zwischen* Zielsystemen | Außerhalb des Jumphost-Scopes | Betrifft die Netzwerkinfrastruktur rund um den Jumphost, nicht den Jumphost selbst — Bestandteil des übergeordneten Netzwerkkonzepts. |
## Bekannte Einschränkungen
- **`pam_faillock`-Einbindung** (`tasks/pam_password_policy.yml`) fügt einen
Block direkt in `/etc/pam.d/common-auth` ein. Auf Systemen, die
`pam-auth-update --force` als Teil eines anderen Automatisierungsschritts
laufen lassen, kann dieser Block überschrieben werden. Für produktive
Systeme mit häufigen PAM-Änderungen wird empfohlen, stattdessen ein
eigenes `pam-auth-update`-Profil unter `/usr/share/pam-configs/` zu
pflegen (sauberer, upgrade-fest) — hier aus Gründen der Nachvollziehbarkeit
als direkter Block-Insert gehalten.
- **`/tmp`/`/dev/shm`-Remount** wirkt nur, wenn diese Pfade bereits eigene
Mountpoints sind. Ist das nicht der Fall, gibt die Rolle eine Debug-Meldung
aus, ändert aber nichts automatisch am Partitionslayout (siehe Tabelle oben).
- Diese Rolle wurde in dieser Session **nicht** gegen ein reales
Debian/Ubuntu-System ausgeführt (kein Root-/Zielsystem verfügbar) — nur
`ansible-playbook --syntax-check` sowie YAML-/Jinja2-Parsing wurden
verifiziert (siehe README, Abschnitt „Was bewusst noch offen ist"). Ein
`--check`-Lauf (Dry-Run) und ein realer Rollout-Test in einer
Staging-Umgebung stehen vor dem Produktivbetrieb noch aus.

View File

@ -0,0 +1,47 @@
---
# Schalter fuer die vertiefte CIS/STIG-nahe Haertung dieser Rolle.
# Siehe ansible/roles/os_hardening/CIS_STIG_MAPPING.md fuer die Zuordnung
# der einzelnen Tasks zu konkreten Benchmark-Controls sowie fuer alles, was
# hier bewusst NICHT automatisiert wird (mit Begruendung).
# AIDE (Dateiintegritaets-Monitoring) initialisieren + taeglichen Check-Timer
# einrichten. Erzeugt beim ersten Lauf spuerbare I/O-Last (voller Datei-Scan).
os_hardening_aide_enabled: true
# rkhunter als zusaetzlicher, ergaenzender Rootkit-/Anomalie-Scanner.
os_hardening_rkhunter_enabled: true
# /tmp und /dev/shm nach Moeglichkeit mit noexec,nosuid,nodev neu einhaengen
# (nur wenn sie BEREITS eigene Mountpoints sind -- siehe Mapping-Dokument
# Punkt "Partitionslayout" fuer den nicht automatisierbaren Teil).
os_hardening_restrict_tmp_mounts: true
# Der Anmelde-Banner-Text (STIG-typischer Warnhinweis). An lokale
# Rechtslage/Policy anpassen.
os_hardening_login_banner: |
******************************************************************
* Autorisierter Zugriff ausschliesslich fuer befugte Personen. *
* Alle Aktivitaeten auf diesem System werden protokolliert und *
* koennen im Rahmen von Sicherheitsuntersuchungen ausgewertet *
* werden. Mit der Anmeldung stimmen Sie dieser Ueberwachung zu. *
******************************************************************
# IPv6 komplett deaktivieren. Standardmaessig AUS (false), da dies in vielen
# Umgebungen ungewollte Nebenwirkungen hat (z.B. wenn Zielsysteme oder das
# Management-Netz IPv6 nutzen) -- bewusst ein Opt-in, kein stiller Default.
os_hardening_disable_ipv6: false
# auditd-Konfiguration als UNVERAENDERLICH markieren (-e 2 am Ende der
# Regeln). Das ist die STIG-konforme Einstellung, erfordert danach aber
# einen Reboot, um die Audit-Regeln ueberhaupt noch aendern zu koennen --
# daher standardmaessig AUS, damit iterative Playbook-Laeufe waehrend des
# Aufbaus nicht versehentlich aussperren. Vor der finalen Abnahme auf true setzen.
os_hardening_auditd_immutable: false
# Passwortrichtlinie fuer LOKALE OS-Konten auf dem Jumphost selbst (NICHT zu
# verwechseln mit der Argon2id/TOTP-Pflicht der Jumphost-WEBANWENDUNG, die
# unabhaengig davon in app/security/passwords.py + totp.py durchgesetzt wird).
os_hardening_password_min_length: 14
os_hardening_password_remember: 5
os_hardening_faillock_deny: 5
os_hardening_faillock_unlock_time: 900

View File

@ -0,0 +1,32 @@
---
- name: restart auditd
ansible.builtin.service:
name: auditd
state: restarted
- name: restart sshd
ansible.builtin.service:
name: ssh
state: restarted
- name: apply sysctl
ansible.builtin.command: sysctl --system
changed_when: true
- name: update initramfs
ansible.builtin.command: update-initramfs -u
changed_when: true
- name: remount tmp
ansible.builtin.command: mount -o remount /tmp
changed_when: true
failed_when: false # best effort, siehe Kommentar in tasks/mounts.yml
- name: remount shm
ansible.builtin.command: mount -o remount /dev/shm
changed_when: true
failed_when: false
- name: reload systemd (os_hardening)
ansible.builtin.systemd:
daemon_reload: true

View File

@ -0,0 +1,86 @@
---
# CIS 1.4.x: AIDE (Advanced Intrusion Detection Environment) als
# Datei-Integritaets-Monitoring. Ergaenzt das Anwendungs-Audit-Log
# (manipulationssichere Hash-Chain, Konzept 6.1) um eine unabhaengige,
# dateisystemweite Kontrolle -- erkennt z.B. Aenderungen an Systembinaries
# oder eingeschleuste Dateien ausserhalb des Anwendungscodes.
- name: AIDE installieren
ansible.builtin.apt:
name: aide
state: present
update_cache: true
when: os_hardening_aide_enabled
- name: Pruefen, ob bereits eine AIDE-Datenbank existiert
ansible.builtin.stat:
path: /var/lib/aide/aide.db
register: _aide_db
when: os_hardening_aide_enabled
- name: Initiale AIDE-Datenbank aufbauen (kann einige Minuten dauern)
ansible.builtin.command: aideinit -y -f
when: os_hardening_aide_enabled and not _aide_db.stat.exists
async: 1800
poll: 30
- name: AIDE-Wrapper-Skript fuer den taeglichen Check ausrollen
ansible.builtin.copy:
dest: /usr/local/sbin/jumphost-aide-check.sh
owner: root
group: root
mode: "0750"
content: |
#!/usr/bin/env bash
set -euo pipefail
LOGFILE=/var/log/jumphost/aide-check.log
/usr/bin/aide.wrapper --check >> "$LOGFILE" 2>&1 || {
logger -p authpriv.warning "AIDE hat Dateisystem-Abweichungen gemeldet, siehe $LOGFILE"
}
when: os_hardening_aide_enabled
- name: systemd-Service fuer taeglichen AIDE-Check
ansible.builtin.copy:
dest: /etc/systemd/system/jumphost-aide-check.service
owner: root
group: root
mode: "0644"
content: |
[Unit]
Description=Taeglicher AIDE-Dateiintegritaets-Check
[Service]
Type=oneshot
ExecStart=/usr/local/sbin/jumphost-aide-check.sh
Nice=10
IOSchedulingClass=idle
when: os_hardening_aide_enabled
notify: reload systemd (os_hardening)
- name: systemd-Timer fuer taeglichen AIDE-Check
ansible.builtin.copy:
dest: /etc/systemd/system/jumphost-aide-check.timer
owner: root
group: root
mode: "0644"
content: |
[Unit]
Description=Taeglicher AIDE-Dateiintegritaets-Check (Timer)
[Timer]
OnCalendar=daily
RandomizedDelaySec=3600
Persistent=true
[Install]
WantedBy=timers.target
when: os_hardening_aide_enabled
notify: reload systemd (os_hardening)
- name: AIDE-Check-Timer aktivieren
ansible.builtin.systemd:
name: jumphost-aide-check.timer
daemon_reload: true
enabled: true
state: started
when: os_hardening_aide_enabled

View File

@ -0,0 +1,98 @@
---
# CIS 4.1.x / DISA-STIG-aehnliche Audit-Regeln. Ueber die bereits vorhandene
# Ueberwachung des Jumphost-Datenverzeichnisses hinaus wird hier ein
# Standard-Ruleset fuer sicherheitsrelevante OS-Ereignisse ergaenzt:
# Identitaets-/Rechteaenderungen, privilegierte Kommandos, Zeit-/
# Netzwerkkonfigurationsaenderungen, Login-Ereignisse, Modulladen.
- name: auditd + audispd-plugins installieren
ansible.builtin.apt:
name:
- auditd
- audispd-plugins
state: present
update_cache: true
- name: Bestehende auditd-Regeln fuer das Jumphost-Datenverzeichnis
ansible.builtin.copy:
dest: /etc/audit/rules.d/10-jumphost-app.rules
owner: root
group: root
mode: "0640"
content: |
-w {{ jumphost_data_dir }}/jumphost.db -p wa -k jumphost_db
-w {{ jumphost_home }} -p wa -k jumphost_app_files
-w /etc/jumphost -p wa -k jumphost_config
notify: restart auditd
- name: Erweiterte CIS/STIG-Audit-Regeln fuer das Basissystem
ansible.builtin.copy:
dest: /etc/audit/rules.d/20-cis-baseline.rules
owner: root
group: root
mode: "0640"
content: |
# Identitaets-/Rechteaenderungen (CIS 4.1.4)
-w /etc/passwd -p wa -k identity
-w /etc/group -p wa -k identity
-w /etc/shadow -p wa -k identity
-w /etc/gshadow -p wa -k identity
-w /etc/sudoers -p wa -k identity
-w /etc/sudoers.d/ -p wa -k identity
# Sudo-Nutzung protokollieren (ergaenzt Defaults logfile in
# sudo_logging.yml um eine auditd-seitige, manipulationsresistentere Spur)
-a always,exit -F arch=b64 -S execve -F euid=0 -F auid!=0 -F auid!=-1 -k privileged_sudo
# Zeitaenderungen (CIS 4.1.3)
-a always,exit -F arch=b64 -S adjtimex,settimeofday,clock_settime -k time_change
-w /etc/localtime -p wa -k time_change
# Netzwerkkonfiguration (CIS 4.1.7)
-w /etc/hosts -p wa -k network_config
-w /etc/network/ -p wa -k network_config
-w /etc/nftables.conf -p wa -k network_config
# Login/Logout-Ereignisse (CIS 4.1.5)
-w /var/log/faillog -p wa -k logins
-w /var/log/lastlog -p wa -k logins
-w /var/run/utmp -p wa -k session
-w /var/log/wtmp -p wa -k session
-w /var/log/btmp -p wa -k session
# Kernel-Modul-Laden/-Entladen (CIS 4.1.13)
-a always,exit -F arch=b64 -S init_module,delete_module -k kernel_modules
# SSH-Konfigurationsaenderungen des Jumphosts selbst
-w /etc/ssh/sshd_config -p wa -k sshd_config
-w /etc/ssh/sshd_config.d/ -p wa -k sshd_config
# Loeschungen durch Nutzer (CIS 4.1.14, exemplarisch fuer den eigenen UID-Bereich)
-a always,exit -F arch=b64 -S unlink,unlinkat,rename,renameat -F auid>=1000 -F auid!=-1 -k file_deletion
notify: restart auditd
- name: auditd-Regeln als unveraenderlich markieren (STIG, optional)
ansible.builtin.copy:
dest: /etc/audit/rules.d/99-immutable.rules
owner: root
group: root
mode: "0640"
content: |
# ACHTUNG: nach Aktivierung sind Aenderungen an den Audit-Regeln erst
# nach einem Reboot wieder moeglich (auditctl -e 2 sperrt bis Neustart).
-e 2
when: os_hardening_auditd_immutable
notify: restart auditd
- name: auditd-Log-Rotation auf "keep_logs" setzen statt Ueberschreiben (CIS 4.1.2.3)
ansible.builtin.lineinfile:
path: /etc/audit/auditd.conf
regexp: '^max_log_file_action\s*='
line: "max_log_file_action = keep_logs"
notify: restart auditd
- name: auditd bei vollem Log-Speicher anhalten statt Ereignisse zu verwerfen (CIS 4.1.2.4)
ansible.builtin.lineinfile:
path: /etc/audit/auditd.conf
regexp: '^space_left_action\s*='
line: "space_left_action = email"

View File

@ -0,0 +1,22 @@
---
# STIG-typische Anmelde-Warnhinweise (rechtlich in vielen Organisationen
# vorgeschrieben, bevor Zugriff auf ein System gewaehrt wird).
- name: Anmelde-Banner setzen (/etc/issue, /etc/issue.net, /etc/motd)
ansible.builtin.copy:
dest: "{{ item }}"
owner: root
group: root
mode: "0644"
content: "{{ os_hardening_login_banner }}"
loop:
- /etc/issue
- /etc/issue.net
- /etc/motd
# Kein Serviceneustart noetig: /etc/issue* und /etc/motd werden bei
# jedem neuen Login/jeder neuen Verbindung frisch gelesen.
# Die "Banner /etc/issue.net"-Direktive fuer sshd wird zentral in sshd.yml
# gesetzt (dieselbe Datei /etc/ssh/sshd_config.d/99-jumphost-hardening.conf
# wird dort komplett -- inkl. Banner-Zeile -- verwaltet, um zwei Tasks mit
# widerspruechlichem "wer besitzt diese Datei" zu vermeiden).

View File

@ -0,0 +1,36 @@
---
# CIS 2.4.1.x: cron/at auf autorisierte Nutzer beschraenken.
- name: cron.deny/at.deny entfernen (deny-Listen sind fehleranfaelliger als allow-Listen)
ansible.builtin.file:
path: "{{ item }}"
state: absent
loop:
- /etc/cron.deny
- /etc/at.deny
- name: cron.allow / at.allow auf root und den Jumphost-Service-User beschraenken
ansible.builtin.copy:
dest: "{{ item }}"
owner: root
group: root
mode: "0600"
content: |
root
loop:
- /etc/cron.allow
- /etc/at.allow
- name: Berechtigungen der cron-Verzeichnisse absichern (CIS 2.4.1.7-2.4.1.11)
ansible.builtin.file:
path: "{{ item }}"
owner: root
group: root
mode: "0700"
loop:
- /etc/cron.d
- /etc/cron.daily
- /etc/cron.hourly
- /etc/cron.monthly
- /etc/cron.weekly
ignore_errors: true # nicht jedes Basis-Image legt alle Verzeichnisse an

View File

@ -0,0 +1,38 @@
---
# CIS 6.1.x: Berechtigungen sicherheitskritischer Systemdateien; zusaetzlich
# manipulationssicheres sudo-Logging (ergaenzt die auditd-Regel
# "privileged_sudo" aus auditd.yml um ein menschenlesbares Log).
- name: Berechtigungen sicherheitskritischer Dateien absichern
ansible.builtin.file:
path: "{{ item.path }}"
owner: root
group: "{{ item.group }}"
mode: "{{ item.mode }}"
loop:
- { path: /etc/passwd, group: root, mode: "0644" }
- { path: /etc/group, group: root, mode: "0644" }
- { path: /etc/shadow, group: shadow, mode: "0640" }
- { path: /etc/gshadow, group: shadow, mode: "0640" }
- { path: /etc/ssh/sshd_config, group: root, mode: "0600" }
ignore_errors: true # z.B. wenn die shadow-Gruppe distributionsabhaengig anders heisst
- name: Eigenstaendiges sudo-Logfile aktivieren
ansible.builtin.copy:
dest: /etc/sudoers.d/99-jumphost-logging
owner: root
group: root
mode: "0440"
validate: "visudo -cf %s"
content: |
Defaults logfile="/var/log/sudo.log"
Defaults log_input, log_output
Defaults use_pty
Defaults passwd_tries=3
- name: su-Kommando auf die Gruppe "sudo" beschraenken (CIS 5.6)
ansible.builtin.lineinfile:
path: /etc/pam.d/su
regexp: '^#?\s*auth\s+required\s+pam_wheel\.so'
line: "auth required pam_wheel.so use_uid group=sudo"
insertafter: '^# Uncomment this'

View File

@ -0,0 +1,41 @@
---
# CIS 1.1.1.x: selten benoetigte Dateisysteme und exotische Netzwerk-
# protokolle per modprobe-Blacklist deaktivieren. Ein dedizierter Jumphost
# braucht keines davon; jedes deaktivierte Modul ist Angriffsflaeche weniger
# (u.a. relevant fuer angeschlossene Wechseldatentraeger und historische
# Kernel-CVEs in selten gepflegten Dateisystemtreibern).
- name: Kernelmodule fuer seltene Dateisysteme/Protokolle blacklisten
ansible.builtin.copy:
dest: /etc/modprobe.d/jumphost-blacklist.conf
owner: root
group: root
mode: "0644"
content: |
# CIS 1.1.1.1 - 1.1.1.8 (sinngemaess): unbenoetigte Dateisysteme
install cramfs /bin/false
install freevxfs /bin/false
install jffs2 /bin/false
install hfs /bin/false
install hfsplus /bin/false
install udf /bin/false
install squashfs /bin/false
# CIS 3.4.x (sinngemaess): unbenoetigte/seltene Netzwerkprotokolle
install dccp /bin/false
install sctp /bin/false
install rds /bin/false
install tipc /bin/false
notify: update initramfs
- name: USB-Speichermedien deaktivieren (CIS 1.1.23, sinngemaess)
ansible.builtin.copy:
dest: /etc/modprobe.d/jumphost-usb-storage.conf
owner: root
group: root
mode: "0644"
content: |
# Ein Jumphost sollte keine lokal angeschlossenen USB-Speichermedien
# einbinden muessen -- Dateitransfer laeuft ausschliesslich ueber die
# Anwendung (SFTP/RDP-Laufwerksumleitung), siehe Konzept 4.3.
install usb-storage /bin/false
notify: update initramfs

View File

@ -0,0 +1,47 @@
---
# Betriebssystem-Haertung fuer den Jumphost-Server selbst (Konzept 6.7),
# vertieft auf CIS/STIG-nahes Niveau. Siehe CIS_STIG_MAPPING.md in diesem
# Rollenverzeichnis fuer die Zuordnung der einzelnen Tasks zu konkreten
# Benchmark-Controls sowie fuer alles, was bewusst NICHT automatisiert wird
# (mit Begruendung, z.B. Partitionslayout, Bootloader-Passwort, physische
# Sicherheit).
#
# Kein Ersatz fuer ein vollstaendiges CIS/STIG-Auditwerkzeug (z.B. OpenSCAP)
# -- jeder produktive Rollout sollte zusaetzlich mit `oscap xccdf eval`
# gegen das jeweilige Benchmark-Profil verifiziert werden (siehe README).
- name: Pakete & automatische Updates
ansible.builtin.import_tasks: packages.yml
- name: Kernelmodule (Dateisysteme/Protokolle) einschraenken
ansible.builtin.import_tasks: kernel_modules.yml
- name: sysctl- und Core-Dump-Haertung
ansible.builtin.import_tasks: sysctl.yml
- name: Mount-Optionen (/tmp, /dev/shm)
ansible.builtin.import_tasks: mounts.yml
- name: PAM-/Passwort-Policy fuer lokale OS-Konten
ansible.builtin.import_tasks: pam_password_policy.yml
- name: Erweiterte auditd-Regeln
ansible.builtin.import_tasks: auditd.yml
- name: AIDE-Dateiintegritaets-Monitoring
ansible.builtin.import_tasks: aide.yml
- name: rkhunter-Rootkit-Scanner
ansible.builtin.import_tasks: rootkit_scan.yml
- name: Anmelde-Banner
ansible.builtin.import_tasks: banners.yml
- name: cron/at auf autorisierte Nutzer beschraenken
ansible.builtin.import_tasks: cron_at.yml
- name: Dateirechte & sudo-Logging
ansible.builtin.import_tasks: file_permissions.yml
- name: SSH-Daemon des Jumphosts haerten
ansible.builtin.import_tasks: sshd.yml

View File

@ -0,0 +1,48 @@
---
# CIS 1.1.2.x (nodev/nosuid/noexec auf /tmp, /dev/shm). Best-Effort: nur
# wirksam, wenn diese Pfade BEREITS eigene Mountpoints sind. Ob das der Fall
# ist, haengt vom Partitionslayout des Basis-Images ab -- siehe
# CIS_STIG_MAPPING.md fuer den Hinweis, dass ein vollstaendig CIS-konformes
# Partitionslayout (separate /tmp, /var, /var/log, /var/log/audit, /home)
# eine bewusste Entscheidung bei der OS-Installation ist und nicht nachtraeglich
# per Ansible auf ein bestehendes System aufgepraegt werden kann, ohne die
# Platte neu zu partitionieren.
- name: Pruefen, ob /tmp ein eigener Mountpoint ist
ansible.builtin.command: findmnt --noheadings --output SOURCE /tmp
register: _tmp_mount
changed_when: false
failed_when: false
- name: /tmp mit noexec,nosuid,nodev in /etc/fstab absichern (falls eigener Mountpoint)
ansible.builtin.lineinfile:
path: /etc/fstab
regexp: '^\S+\s+/tmp\s+'
line: "{{ _tmp_mount.stdout }} /tmp tmpfs defaults,noexec,nosuid,nodev 0 0"
backup: true
when: os_hardening_restrict_tmp_mounts and _tmp_mount.rc == 0 and _tmp_mount.stdout != ''
notify: remount tmp
- name: Pruefen, ob /dev/shm ein eigener Mountpoint ist
ansible.builtin.command: findmnt --noheadings --output SOURCE /dev/shm
register: _shm_mount
changed_when: false
failed_when: false
- name: /dev/shm mit noexec,nosuid,nodev in /etc/fstab absichern (falls eigener Mountpoint)
ansible.builtin.lineinfile:
path: /etc/fstab
regexp: '^\S+\s+/dev/shm\s+'
line: "{{ _shm_mount.stdout }} /dev/shm tmpfs defaults,noexec,nosuid,nodev 0 0"
backup: true
when: os_hardening_restrict_tmp_mounts and _shm_mount.rc == 0 and _shm_mount.stdout != ''
notify: remount shm
- name: Hinweis, falls /tmp kein eigener Mountpoint ist
ansible.builtin.debug:
msg: >
/tmp ist kein eigener Mountpoint auf diesem System -- noexec/nosuid/nodev
koennen so nicht erzwungen werden. Fuer volle CIS-Konformitaet muesste
/tmp bei der OS-Installation als eigene Partition/eigenes tmpfs angelegt
werden (siehe CIS_STIG_MAPPING.md).
when: os_hardening_restrict_tmp_mounts and (_tmp_mount.rc != 0 or _tmp_mount.stdout == '')

View File

@ -0,0 +1,33 @@
---
# CIS 2.x (Services) / STIG-aequivalent: unsichere Legacy-Dienste/-Clients
# entfernen, automatische Sicherheitsupdates aktivieren.
- name: Unnoetige/unsichere Pakete entfernen
ansible.builtin.apt:
name: "{{ item }}"
state: absent
purge: true
loop:
- telnet
- rsh-client
- talk
- nis # ypbind etc. -- veraltete, unverschluesselte Netzwerkdienste
- tftpd-hpa
- xinetd
ignore_errors: true
- name: Automatische Sicherheitsupdates installieren
ansible.builtin.apt:
name: unattended-upgrades
state: present
update_cache: true
- name: Unattended-upgrades aktivieren
ansible.builtin.copy:
dest: /etc/apt/apt.conf.d/20auto-upgrades
owner: root
group: root
mode: "0644"
content: |
APT::Periodic::Update-Package-Lists "1";
APT::Periodic::Unattended-Upgrade "1";

View File

@ -0,0 +1,83 @@
---
# CIS 5.3.x / 5.4.x: Passwortqualitaet, Account-Lockout und Ablaufregeln fuer
# LOKALE OS-Konten auf dem Jumphost selbst (Admin-SSH-Zugang zum Jumphost-
# Server, siehe ssh_admin_access_cidr in group_vars/all.yml).
#
# WICHTIG -- Abgrenzung: Dies ist NICHT identisch mit der Argon2id/TOTP-
# Pflicht der Jumphost-WEBANWENDUNG (siehe app/security/passwords.py,
# app/security/totp.py, app/auth/routes.py). Es handelt sich um zwei
# getrennte Konten-/Auth-Systeme: die App verwaltet ihre eigenen Nutzer in
# SQLite, waehrend hier die BS-Konten der Administratoren gehaertet werden,
# die sich per SSH auf den Jumphost-Server selbst einloggen (z.B. fuer
# Wartung, Deployment, Log-Einsicht).
- name: libpam-pwquality installieren
ansible.builtin.apt:
name: libpam-pwquality
state: present
update_cache: true
- name: Passwortqualitaets-Policy setzen (CIS 5.4.1)
# Direkt in pwquality.conf statt eines conf.d-Snippets, da nicht jede
# Distributionsversion von libpam-pwquality ein conf.d-Verzeichnis
# unterstuetzt -- pwquality.conf selbst wird ueberall gelesen.
ansible.builtin.lineinfile:
path: /etc/security/pwquality.conf
regexp: "^#?\\s*{{ item.key }}\\s*="
line: "{{ item.key }} = {{ item.value }}"
create: true
owner: root
group: root
mode: "0644"
loop:
- { key: "minlen", value: "{{ os_hardening_password_min_length }}" }
- { key: "dcredit", value: "-1" }
- { key: "ucredit", value: "-1" }
- { key: "ocredit", value: "-1" }
- { key: "lcredit", value: "-1" }
- { key: "retry", value: "3" }
# Kein Service-Neustart noetig: PAM liest die Datei bei jeder neuen
# Authentifizierung, kein laufender Daemon haelt sie offen.
- name: Passwortqualitaet auch fuer root erzwingen
ansible.builtin.lineinfile:
path: /etc/security/pwquality.conf
regexp: "^#?\\s*enforce_for_root"
line: "enforce_for_root"
create: true
- name: pam_faillock fuer Login-Lockout aktivieren (CIS 5.3.1)
ansible.builtin.blockinfile:
path: /etc/pam.d/common-auth
marker: "# {mark} ANSIBLE MANAGED BLOCK (jumphost pam_faillock)"
insertbefore: "^auth\\s+\\[success=1"
block: |
auth required pam_faillock.so preauth silent deny={{ os_hardening_faillock_deny }} unlock_time={{ os_hardening_faillock_unlock_time }}
auth [success=1 default=ignore] pam_unix.so nullok
auth [default=die] pam_faillock.so authfail deny={{ os_hardening_faillock_deny }} unlock_time={{ os_hardening_faillock_unlock_time }}
auth sufficient pam_faillock.so authsucc deny={{ os_hardening_faillock_deny }} unlock_time={{ os_hardening_faillock_unlock_time }}
# Hinweis: pam-auth-update-verwaltete Systeme (Debian/Ubuntu-Standard)
# ueberschreiben common-auth ggf. bei "pam-auth-update --force". Fuer
# produktive Systeme ist die Nutzung eines eigenen pam-auth-update-Profils
# (/usr/share/pam-configs/jumphost-faillock) die sauberere, upgrade-feste
# Alternative -- hier aus Uebersichtlichkeitsgruenden als direkter Block-
# Insert gehalten und im Mapping-Dokument als bekannte Einschraenkung vermerkt.
- name: Passwort-Ablaufregeln in /etc/login.defs setzen (CIS 5.4.1.1-5.4.1.4)
ansible.builtin.lineinfile:
path: /etc/login.defs
regexp: "^{{ item.key }}\\s"
line: "{{ item.key }} {{ item.value }}"
loop:
- { key: "PASS_MAX_DAYS", value: "90" }
- { key: "PASS_MIN_DAYS", value: "7" }
- { key: "PASS_WARN_AGE", value: "14" }
- { key: "UMASK", value: "027" }
- { key: "ENCRYPT_METHOD", value: "SHA512" }
- name: Passwort-Historie (pam_pwhistory) aktivieren (CIS 5.4.2)
ansible.builtin.lineinfile:
path: /etc/pam.d/common-password
regexp: '^password\s+requisite\s+pam_pwhistory\.so'
insertafter: '^password\s+requisite\s+pam_pwquality\.so'
line: "password requisite pam_pwhistory.so remember={{ os_hardening_password_remember }} use_authtok"

View File

@ -0,0 +1,24 @@
---
# Ergaenzender Rootkit-/Anomalie-Scanner (rkhunter). Kein Ersatz fuer AIDE
# (dateibasierte Integritaet) oder auditd (Ereignisprotokoll), sondern eine
# dritte, unabhaengige Kontrollschicht mit eigener Signaturheuristik.
- name: rkhunter installieren
ansible.builtin.apt:
name: rkhunter
state: present
update_cache: true
when: os_hardening_rkhunter_enabled
- name: rkhunter-Property-Datenbank initialisieren
ansible.builtin.command: rkhunter --propupd
when: os_hardening_rkhunter_enabled
changed_when: true
- name: Woechentlichen rkhunter-Check per systemd-Timer aktivieren (Debian-Paket-Default nutzen)
ansible.builtin.lineinfile:
path: /etc/default/rkhunter
regexp: '^CRON_DAILY_RUN='
line: 'CRON_DAILY_RUN="true"'
create: true
when: os_hardening_rkhunter_enabled

View File

@ -0,0 +1,32 @@
---
# SSH-Daemon des Jumphosts SELBST (administrativer Zugriff auf den Server) --
# nicht zu verwechseln mit der SSH-Proxy-Funktion der Anwendung (app/ssh_proxy),
# die eigene, unabhaengige Verbindungen zu den Zielsystemen aufbaut.
# CIS 5.2.x.
- name: SSH-Daemon des Jumphosts haerten (vollstaendige Konfiguration inkl. Banner)
ansible.builtin.copy:
dest: /etc/ssh/sshd_config.d/99-jumphost-hardening.conf
owner: root
group: root
mode: "0644"
content: |
PermitRootLogin no
PasswordAuthentication no
KbdInteractiveAuthentication no
PermitEmptyPasswords no
X11Forwarding no
AllowTcpForwarding no
AllowAgentForwarding no
PermitTunnel no
MaxAuthTries 3
MaxSessions 4
LoginGraceTime 20
ClientAliveInterval 300
ClientAliveCountMax 2
Banner /etc/issue.net
LogLevel VERBOSE
Ciphers chacha20-poly1305@openssh.com,aes256-gcm@openssh.com,aes128-gcm@openssh.com
KexAlgorithms curve25519-sha256,curve25519-sha256@libssh.org,diffie-hellman-group16-sha512
MACs hmac-sha2-512-etm@openssh.com,hmac-sha2-256-etm@openssh.com
notify: restart sshd

View File

@ -0,0 +1,58 @@
---
# CIS 3.x (Netzwerk) / 1.5.x (Kernel-Haertung). Bewusst ueber
# ansible.builtin.copy + "sysctl --system" statt des ansible.posix.sysctl-
# Moduls (siehe Kommentar in Kap. 7/README) -- keine Zusatz-Collection als
# Voraussetzung fuer den Betrieb dieses Playbooks.
- name: Kernel-/Netzwerk-Haertung (sysctl)
ansible.builtin.copy:
dest: /etc/sysctl.d/99-jumphost-hardening.conf
owner: root
group: root
mode: "0644"
content: |
# --- Netzwerk (CIS 3.x) ---
net.ipv4.conf.all.rp_filter = 1
net.ipv4.conf.default.rp_filter = 1
net.ipv4.conf.all.accept_redirects = 0
net.ipv4.conf.default.accept_redirects = 0
net.ipv4.conf.all.secure_redirects = 0
net.ipv4.conf.default.secure_redirects = 0
net.ipv4.conf.all.send_redirects = 0
net.ipv4.conf.default.send_redirects = 0
net.ipv4.conf.all.accept_source_route = 0
net.ipv4.conf.default.accept_source_route = 0
net.ipv4.conf.all.log_martians = 1
net.ipv4.conf.default.log_martians = 1
net.ipv4.icmp_echo_ignore_broadcasts = 1
net.ipv4.icmp_ignore_bogus_error_responses = 1
net.ipv4.tcp_syncookies = 1
net.ipv4.ip_forward = 0
net.ipv6.conf.all.accept_redirects = 0
net.ipv6.conf.default.accept_redirects = 0
net.ipv6.conf.all.accept_source_route = 0
net.ipv6.conf.default.accept_source_route = 0
{% if os_hardening_disable_ipv6 %}
net.ipv6.conf.all.disable_ipv6 = 1
net.ipv6.conf.default.disable_ipv6 = 1
{% endif %}
# --- Kernel-Haertung (CIS 1.5.x) ---
kernel.kptr_restrict = 2
kernel.dmesg_restrict = 1
kernel.randomize_va_space = 2
kernel.yama.ptrace_scope = 1
fs.suid_dumpable = 0
fs.protected_hardlinks = 1
fs.protected_symlinks = 1
fs.protected_fifos = 2
fs.protected_regular = 2
notify: apply sysctl
- name: Core Dumps zusaetzlich auf PAM-Ebene deaktivieren (CIS 1.5.1)
ansible.builtin.copy:
dest: /etc/security/limits.d/99-jumphost-no-coredumps.conf
owner: root
group: root
mode: "0644"
content: |
* hard core 0

View File

@ -0,0 +1,8 @@
---
- name: restart jumphost-app
ansible.builtin.service:
name: jumphost-app
state: restarted
# Handler laeuft ggf. bevor der Service durch jumphost_app angelegt wurde;
# daher failed_when: false beim allerersten Deploy.
failed_when: false

View File

@ -0,0 +1,93 @@
---
- name: Systembenutzer fuer die Jumphost-App anlegen
ansible.builtin.group:
name: "{{ jumphost_app_group }}"
system: true
- name: Systembenutzer anlegen (kein Login-Shell, kein Home mit Zugriff fuer andere)
ansible.builtin.user:
name: "{{ jumphost_app_user }}"
group: "{{ jumphost_app_group }}"
system: true
shell: /usr/sbin/nologin
home: "{{ jumphost_home }}"
create_home: false
- name: Python 3 + venv-Paket installieren
ansible.builtin.apt:
name:
- python3
- python3-venv
- python3-pip
state: present
update_cache: true
- name: Verzeichnisse anlegen
ansible.builtin.file:
path: "{{ item }}"
state: directory
owner: "{{ jumphost_app_user }}"
group: "{{ jumphost_app_group }}"
mode: "0750"
loop:
- "{{ jumphost_home }}"
- "{{ jumphost_data_dir }}"
- "{{ jumphost_data_dir }}/recordings"
- /run/jumphost
- /var/log/jumphost
- name: Alten Anwendungscode entfernen (sauberes Redeploy)
ansible.builtin.file:
path: "{{ jumphost_home }}/{{ item }}"
state: absent
loop:
- app
- static
- templates
- name: Anwendungscode kopieren
# Bewusst ueber ansible.builtin.copy statt ansible.posix.synchronize, damit
# das Playbook ohne zusaetzliche Collection auskommt und auch ohne rsync
# auf Control-Node/Zielsystem funktioniert (Konzept-Anspruch: minimale
# externe Abhaengigkeiten fuer den Deploy-Pfad selbst).
ansible.builtin.copy:
src: "{{ jumphost_repo_src }}/{{ item }}/"
dest: "{{ jumphost_home }}/{{ item }}/"
owner: "{{ jumphost_app_user }}"
group: "{{ jumphost_app_group }}"
loop:
- app
- static
- templates
notify: restart jumphost-app
- name: requirements.txt kopieren
ansible.builtin.copy:
src: "{{ jumphost_repo_src }}/requirements.txt"
dest: "{{ jumphost_home }}/requirements.txt"
owner: "{{ jumphost_app_user }}"
group: "{{ jumphost_app_group }}"
notify: restart jumphost-app
- name: Dateirechte auf Anwendungscode setzen
ansible.builtin.file:
path: "{{ jumphost_home }}"
state: directory
owner: "{{ jumphost_app_user }}"
group: "{{ jumphost_app_group }}"
recurse: true
- name: Virtualenv anlegen
ansible.builtin.command:
cmd: "python3 -m venv {{ jumphost_venv }}"
creates: "{{ jumphost_venv }}/bin/python"
become: true
become_user: "{{ jumphost_app_user }}"
- name: Python-Abhaengigkeiten installieren
ansible.builtin.pip:
requirements: "{{ jumphost_home }}/requirements.txt"
virtualenv: "{{ jumphost_venv }}"
become: true
become_user: "{{ jumphost_app_user }}"
notify: restart jumphost-app

View File

@ -0,0 +1,25 @@
---
# Die Migrationen laufen bereits automatisch beim App-Start (app/db.py:init_db).
# Diese Rolle stellt lediglich sicher, dass Verzeichnis/Rechte VOR dem ersten
# Start korrekt sind und legt optional den initialen Admin-User an.
- name: Datenverzeichnis-Rechte sicherstellen
ansible.builtin.file:
path: "{{ jumphost_data_dir }}"
state: directory
owner: "{{ jumphost_app_user }}"
group: "{{ jumphost_app_group }}"
mode: "0700"
- name: Pruefen, ob bereits eine DB existiert
ansible.builtin.stat:
path: "{{ jumphost_data_dir }}/jumphost.db"
register: _db_stat
- name: Hinweis fuer manuellen Erstadmin-Anlegeschritt ausgeben
ansible.builtin.debug:
msg: >
Keine bestehende Datenbank gefunden. Nach dem ersten Start des Dienstes
(Rolle jumphost_app) einmalig ausfuehren:
{{ jumphost_venv }}/bin/python {{ jumphost_home }}/scripts/create_admin.py --username admin
when: not _db_stat.stat.exists

View File

@ -0,0 +1,85 @@
---
# Drei TLS-Modi (Konzept 7.2a), gesteuert ueber tls_mode. Ergebnis ist in
# allen Faellen identisch: /etc/jumphost/tls/server.{crt,key} bzw. bei
# external_reverse_proxy zusaetzlich das interne Re-Encryption-Zertifikat
# fuer nginx unter demselben Pfad (nginx_proxy-Rolle nutzt diese Dateien).
- name: TLS-Verzeichnis anlegen
ansible.builtin.file:
path: /etc/jumphost/tls
state: directory
owner: root
group: "{{ jumphost_app_group if not enable_nginx_proxy else 'root' }}"
mode: "0750"
- name: "Modus internal_pki / external_reverse_proxy: Zertifikat per ACME gegen interne CA beziehen"
block:
- name: step-ca ACME-Client (step-cli) installieren
ansible.builtin.apt:
name: step-cli
state: present
ignore_errors: true # Paket ist nicht in allen Distros verfuegbar, siehe Fallback unten
- name: Hinweis auf manuellen/alternativen Cert-Bezug
ansible.builtin.debug:
msg: >
Falls step-cli nicht verfuegbar ist: Zertifikat/Key manuell aus der
Unternehmens-PKI beziehen und ALS ansible-vault-verschluesselte
Dateien unter files/tls/{{ inventory_hostname }}.crt / .key ablegen;
diese Rolle kopiert sie dann per copy-Task (siehe unten, auskommentiert).
when: tls_mode in ['internal_pki', 'external_reverse_proxy']
# Alternative zu ACME: vorab per Unternehmens-PKI ausgestellte Dateien einspielen.
# - name: Vorab ausgestelltes internes Zertifikat einspielen
# ansible.builtin.copy:
# src: "files/tls/{{ inventory_hostname }}.crt"
# dest: /etc/jumphost/tls/server.crt
# when: tls_mode in ['internal_pki', 'external_reverse_proxy']
- name: "Modus acme_public: Certbot fuer oeffentliches Let's-Encrypt-Zertifikat"
block:
- name: certbot installieren
ansible.builtin.apt:
name: certbot
state: present
- name: Zertifikat beziehen (nginx-Plugin, HTTP-01)
ansible.builtin.command:
cmd: >
certbot certonly --nginx --non-interactive --agree-tos
-m {{ acme_email }} -d {{ acme_domain }}
creates: "/etc/letsencrypt/live/{{ acme_domain }}/fullchain.pem"
when: enable_nginx_proxy
- name: Symlinks fuer einheitlichen Pfad anlegen
ansible.builtin.file:
src: "/etc/letsencrypt/live/{{ acme_domain }}/{{ item.src }}"
dest: "/etc/jumphost/tls/{{ item.dest }}"
state: link
loop:
- { src: fullchain.pem, dest: server.crt }
- { src: privkey.pem, dest: server.key }
- name: Certbot-Renew-Timer aktivieren
ansible.builtin.service:
name: certbot.timer
state: started
enabled: true
when: tls_mode == 'acme_public'
- name: Warnung bei Klartext-internem Hop protokollieren (bewusste Ausnahme, Konzept 7.2a)
ansible.builtin.debug:
msg: >
ACHTUNG: internal_hop_plaintext_accepted=true gesetzt -- die Strecke
zwischen externem Reverse Proxy und diesem Jumphost laeuft unverschluesselt.
Dies ist NICHT die Standardempfehlung und erfordert eine dokumentierte
Risikoakzeptanz (siehe Konzept 7.2a).
when: tls_mode == 'external_reverse_proxy' and internal_hop_plaintext_accepted | default(false)
- name: Zertifikatsdateien-Rechte absichern
ansible.builtin.file:
path: /etc/jumphost/tls
owner: root
group: "{{ jumphost_app_group if not enable_nginx_proxy else 'root' }}"
mode: "0750"
recurse: true

23
ansible/site.yml Normal file
View File

@ -0,0 +1,23 @@
---
# Jumphost-Deployment. Siehe Konzeptdokument Kap. 7 fuer Hintergrund.
#
# Steuerung ueber group_vars:
# enable_nginx_proxy: true|false (Kap. 7.2)
# tls_mode: internal_pki | external_reverse_proxy | acme_public (Kap. 7.2a)
- hosts: jumphosts
become: true
vars_files:
- inventory/group_vars/all.yml
roles:
- os_hardening
- firewall_nftables
- fail2ban
- frontend_assets # baut static/js/vendor/* lokal VOR dem Sync in python_runtime
- python_runtime
- sqlite_init
- guacd
- jumphost_app
- tls_certificates
- { role: nginx_proxy, when: enable_nginx_proxy | default(false) }
- backup