Foreman
Foreman ist in dieser Architektur der zentrale Punkt, an dem Hosts inventarisiert und mit Puppet-Klassen versehen werden. Eine frisch provisionierte VM bootet, ein Cloud-Init-Skript installiert den Puppet-Agent, der Agent meldet sich beim Foreman und holt sich von dort die Klassen-Zuweisung. Sobald die Pflicht-Parameter im Foreman ausgefüllt und die zugehörigen Secrets in Vault hinterlegt sind, läuft das Deployment durch.
Dieses Dokument beschränkt sich auf die Teile, die für die Deployment-Automation relevant sind. Allgemeine Foreman/Puppet-Einrichtung steht in Foreman & Puppet Setup.
Foreman-Instanz
| Endpoint | Wert |
|---|---|
| Web-UI / API | https://foreman.iteas.tools |
| Puppet-Server (Agent-Endpoint) | foreman.iteas.tools (Port 8140) |
Cloud-Init- und Agent-Konfigurationen zeigen auf diesen Hostnamen.
Cloud-Init Bootstrap
Beim ersten Boot der VM führt das Cloud-Init-Skript drei Schritte aus:
- Puppet-Agent installieren aus dem offiziellen Puppet-Repository (
apt.puppet.combzw.yum.puppet.com, je nach Distribution). - Beim Foreman anmelden über
server = foreman.iteas.tools. Der erste Agent-Run schickt einen Certificate-Signing-Request; den signiert der Foreman per Auto-Sign oder manuell (siehe Certificate Signing). - Environment
iteasappszuweisen überenvironment = iteasappsin/etc/puppetlabs/puppet/puppet.conf. Nach dem Cert-Signing übernimmt Foreman die Klassifizierung beim nächsten Agent-Run.
Erst nach erfolgreichem Bootstrap erscheint der Host in der Foreman-UI und kann dort klassifiziert werden.
Das Skript liegt im Modul-Repository unter cloud-init/iteasapps-bootstrap.yaml. Foreman-Hostname und Environment sind hartcodiert, das File lässt sich unverändert als User-Data hinterlegen.
Manuelles Bootstrapping unter Proxmox
Proxmox kann kein vollständiges Cloud-Init-User-Data
Die Proxmox-Cloud-Init-Integration deckt nur einen festen Subset ab (Hostname, SSH-Keys, Netzwerk). Ein #cloud-config-File mit runcmd-Block lässt sich über die Proxmox-UI nicht als User-Data hinterlegen — iteasapps-bootstrap.yaml läuft beim ersten Boot also nicht automatisch durch. Auf Proxmox-VMs sind die drei Bootstrap-Schritte einmalig von Hand auszuführen.
Der manuelle Ablauf entspricht dem Skript:
Per SSH auf die VM verbinden (Default-User und Key über das Proxmox-Cloud-Init-Modul).
Puppet-Agent installieren, distributionsabhängig:
Debian/Ubuntu:
bash. /etc/os-release wget -O /tmp/puppet8-release.deb "https://apt.puppet.com/puppet8-release-${VERSION_CODENAME}.deb" sudo dpkg -i /tmp/puppet8-release.deb sudo apt-get update && sudo apt-get install -y puppet-agentDas
. /etc/os-releaseliest den Distributions-Codenamen (z.B.bookworm,noble) aus der vom System selbst gepflegtenos-release-Datei in$VERSION_CODENAMEein — die URL wird also automatisch passend zur Distribution gebildet, nichts vorher nachschlagen. Wenn der Download mit 404 antwortet, hat Puppet für den Codenamen noch kein Release-Meta-Paket gebaut; in dem Fall direkt unter apt.puppet.com den nächst-älteren Codenamen heraussuchen und einsetzen.Rocky/CentOS/RHEL:
bashsudo rpm -Uvh "https://yum.puppet.com/puppet8-release-el-$(rpm -E '%{rhel}').noarch.rpm" sudo dnf install -y puppet-agentAnalog liefert
rpm -E '%{rhel}'die RHEL-Major-Version (8,9) — auch hier kein manuelles Nachschlagen nötig.Agent konfigurieren und ersten Run anstoßen:
bashsudo tee /etc/puppetlabs/puppet/puppet.conf >/dev/null <<EOF [main] server = foreman.iteas.tools ca_server = foreman.iteas.tools environment = development certname = $(hostname -f) runinterval = 30m EOF # UTF-8-Locale — zwei Layer, beide notwendig: # # 1. System-Locale → greift für interaktive Shells, `sudo puppet # agent --test`, Cron, alles was über PAM/login startet. # 2. systemd-Drop-in → greift speziell für die puppet.service-Unit, # die /etc/default/locale nicht liest. # # Ohne UTF-8 bricht Ruby beim Capture von Exec-Output mit Nicht- # ASCII-Bytes ab (`invalid byte sequence in US-ASCII`). sudo tee /etc/default/locale >/dev/null <<'EOF' LANG=C.UTF-8 LC_ALL=C.UTF-8 EOF sudo mkdir -p /etc/systemd/system/puppet.service.d sudo tee /etc/systemd/system/puppet.service.d/locale.conf >/dev/null <<'EOF' [Service] Environment="LANG=C.UTF-8" Environment="LC_ALL=C.UTF-8" EOF sudo systemctl daemon-reload # Den ersten Puppet-Lauf in einer Shell starten, die die neue Locale # bereits geladen hat — sonst erbt der manuelle Run noch das alte # `C` aus der bestehenden SSH-Session: export LANG=C.UTF-8 LC_ALL=C.UTF-8 sudo /opt/puppetlabs/bin/puppet agent --test --waitforcert 10 sudo systemctl enable --now puppet.servicepuppet agent --testschickt den CSR an den Foreman. Bei abgeschaltetem Auto-Sign muss das Zertifikat dort manuell signiert werden, bevor der Run durchläuft.Reihenfolge beachten
Erst der interaktive
puppet agent --test-Lauf, dannsystemctl enable --now. Ist der Service schon aktiv, wenn der manuelle Lauf startet, kollidieren beide am Agent-Lock und der manuelle Lauf bricht mitAnother puppet instance is already runningab. Sollte der Service bereits laufen:sudo systemctl stop puppetdavor,startdanach.
Andere Provider
Auf Plattformen mit vollständiger Cloud-Init-Unterstützung (OpenStack, AWS EC2, Hetzner Cloud, …) wird iteasapps-bootstrap.yaml als User-Data hinterlegt und läuft beim ersten Boot automatisch — der manuelle Block oben entfällt.
Puppet-Environment iteasapps
Alle über diesen Prozess provisionierten Hosts laufen im Puppet-Environment iteasapps. Es enthält das gleichnamige Modul mit sämtlichen Klassen, Defined Types und Templates:
Die Bindung des Moduls an das Environment erfolgt über das Puppetfile im puppet-control-Repository.
Klassen-Struktur
Das Modul folgt dem Roles-&-Profiles-Pattern: eine Basis-Klasse, die immer ausgerollt wird, plus Applikations-Klassen, die sich pro Host beliebig kombinieren lassen.
iteasapps::base
Bündelt alles, was App-übergreifend einmal pro Host eingerichtet wird. Jede App-Klasse zieht die Basis per include ein; bei mehreren App-Klassen auf demselben Host läuft sie trotzdem nur einmal, da include idempotent ist.
Die Baselines selbst liegen im Schwester-Modul puppet-module-services (Namespace iteas_services::*), weil sie nicht iScan-/iTransfer-spezifisch sind und auch andere Module sie wiederverwenden. iteasapps::base ist nur die Sammelklasse, die sie einbindet:
| Sub-Klasse | Aufgabe |
|---|---|
iteas_services::nginx | Nginx-Grundkonfiguration, globale Rate-Limit-Zonen, GeoIP-Map |
iteas_services::php_fpm | PHP 8.4 mit PHP-FPM (aus Ondrej-PPA), Standard-Module |
iteas_services::mariadb | MariaDB-Server mit TLS-Konfiguration |
iteas_services::fail2ban | Fail2Ban für statuscode-basierte IP-Bans |
iteas_services::geoip | MaxMind GeoIP2-Datenbank und geoipupdate |
Applikations-Klassen
Pro deploybare Applikation existiert eine Klasse, die im Foreman direkt klassifiziert wird:
| Klasse | Beschreibung |
|---|---|
iteasapps::iscan | Deployt eine iScan-Instanz |
iteasapps::itransfer | Deployt eine iTransfer-Instanz |
Mehrere Klassen können am selben Host nebeneinander laufen. Die Basis bleibt geteilt; die App-spezifischen Ressourcen (System-User, FPM-Pool, VHost, Datenbank) werden pro Klasse instanziert.
Daneben existiert eine Host-weite Service-Klasse, die nicht zu einer einzelnen Applikation gehört, sondern eine Eigenschaft der MariaDB-Instanz auf dem Host beschreibt:
| Klasse | Beschreibung |
|---|---|
iteas_services::edbs | EDBS-Anbindung: legt den MariaDB-Login für eingehende Verbindungen des EDBS-Servers an (siehe EDBS-Anbindung). Wird einmal pro Host klassifiziert, unabhängig davon, wie viele iScan- oder iTransfer-Instanzen am gleichen Host laufen. |
Defined Types
Interne Bausteine, aus denen die App-Klassen ihre Ressourcen zusammensetzen. Werden nicht direkt klassifiziert:
| Defined Type | Aufgabe |
|---|---|
iteasapps::webhost | System-User, FPM-Pool, Nginx-VHost, TLS-Zertifikat (Certbot) |
iteasapps::release | Lädt das App-Tarball aus der GitLab Package Registry, deployt nach /var/www/<app>/, ruft Yii-Migrationen auf |
iteasapps::healthcheck | Smoke-Test der ausgerollten Applikation |
iteas_services::sql::login | MariaDB-Login, optional mit REQUIRE SSL (im Schwester-Modul) |
iteas_services::firewall::sql_port | Öffnet den DB-Port für eine bestimmte Quell-IP (im Schwester-Modul) |
Smart Class Parameters
Werte, die sich pro Host unterscheiden — Domain, Logins, Whitelists —, werden im Foreman als Smart Class Parameters gepflegt. Parameter ohne Default sind Pflicht; Foreman erzwingt die Eingabe. Parameter mit Default sind optional.
Die Parameter der iScan-Klasse decken die Environment-Variablen ab, die der iScan-Container laut docker-compose erwartet. Im docker-compose leere Werte ('') sind hier Pflicht; Werte mit Default werden mit demselben Default vorbelegt.
Pflicht-Parameter
| Parameter | Typ | Beschreibung |
|---|---|---|
domain | String | FQDN der Instanz |
edbs_db_user | String | Username für die Remote-EDBS-DB (outbound) |
edbs_db_host | String | Host (bzw. host:port) der Remote-EDBS-DB |
edbs_db_name | String | Datenbank-Name der Remote-EDBS-DB |
Webhost / Infrastruktur
| Parameter | Typ | Default | Beschreibung |
|---|---|---|---|
system_user | String | f01_iscan | Linux-System-User |
db_user | String | iscan | Lokaler MariaDB-Login (= DB_USER) und gleichzeitig DB-Name (= DB_NAME) |
db_hostname | String | localhost | Hostname, unter dem die App die DB anspricht (= DB_HOSTNAME) |
allowed_countries | Array[String] | ['AT', 'DE', 'CH'] | Geoblocking-Whitelist |
mode | Enum | native | native (FPM-Pool) oder containerized (Docker-Container) |
container_port | Optional[Integer] | — | Nur bei mode = containerized |
App-Konfiguration (Yii / Customer / Mail)
| Parameter | Typ | Default | Env-Var |
|---|---|---|---|
yii_environment | Enum['dev','prod'] | dev | YII_ENVIRONMENT |
yii_debug | Boolean | true | YII_DEBUG |
customer | String | iteas | CUSTOMER |
sender_mail_address | String | noreply@iscan.at | SENDER_MAIL_ADDRESS |
sp_sender_mail_address | String | noreply@opst.at | SP_SENDER_MAIL_ADDRESS |
sp_user_invite_valid_hour | Integer | 12 | SP_USER_INVITE_VALID_HOUR |
enable_schema_cache | Boolean | true | ENABLE_SCHEMA_CACHE |
Logging (Graylog & Loki)
| Parameter | Typ | Default | Env-Var |
|---|---|---|---|
enable_logging | Boolean | false | ENABLE_LOGGING |
graylog_facility | String | iscan-dev | GRAYLOG_FACILITY |
graylog_url | String | gelf.log.iteas.at | GRAYLOG_URL |
graylog_port | Integer | 443 | GRAYLOG_PORT |
loki_enable_logging | Boolean | false | LOKI_ENABLE_LOGGING |
loki_api_url | String | https://api.log.iteas.cloud/loki/api/v1/push | LOKI_API_URL |
loki_service_name | String | iscan-web | LOKI_SERVICE_NAME |
Der lokale App-DB-Login verbindet sich über localhost und ist daher nicht TLS-pflichtig. Die andere EDBS-Richtung — der MariaDB-Login m01_edbs für eingehende Verbindungen vom EDBS-Server — gehört nicht in diese Klasse, sondern in iteas_services::edbs (siehe EDBS-Anbindung).
Zwei Richtungen der EDBS-Anbindung
Es gibt zwei voneinander unabhängige EDBS-Verbindungen:
- Outbound (in dieser Klasse): iScan verbindet sich zur Remote-EDBS-DB. Credentials =
edbs_db_*+ Vault-Secretedbs_db_password. - Inbound (
iteas_services::edbs): Der externe EDBS-Server verbindet sich zur lokalen MariaDB. Credentials = MariaDB-Userm01_edbs+ Vault-Secret untersecret/iteas_services/edbs/password.
Da pro Host nur ein Kunde betrieben wird, steckt die Kundenkennung im Hostnamen bzw. Puppet-Certname und nicht in den Resource-Identifiern.
Globale Defaults für Host-weite Werte (Rate-Limit-Vorgaben, GeoIP-Account, MariaDB-Tuning) liegen in den iteasapps::base::*-Klassen und sind ebenfalls über Foreman überschreibbar.
Default-Werte der Basis-Klassen
iteasapps::base::nginx — Rate-Limit-Zonen
| Parameter | Default | Beschreibung |
|---|---|---|
rate_general | 60r/m | Standard-Limit für alle Routen |
rate_api | 10r/s | Limit für REST/API-Routen |
rate_login | 5r/m | Limit für Auth-Endpoints |
burst_general | 20 | Burst-Tolerance für general |
burst_api | 20 | Burst-Tolerance für api |
burst_login | 3 | Burst-Tolerance für login |
zone_size | 10m | Shared-Memory-Größe pro Zone |
limit_req_status | 429 | Antwort-Code bei Rate-Limit-Verletzung |
iteasapps::base::php — PHP-FPM
| Parameter | Default | Beschreibung |
|---|---|---|
php_version | 8.4 | PHP-Hauptversion (aus Ondrej-PPA) |
php_modules | ['mysql','curl','mbstring','xml','intl'] | Pflicht-Module für die Iteas-Apps |
pm_max_children | 20 | FPM-Pool: maximale Worker pro Pool |
pm_start_servers | 4 | FPM-Pool: initial gestartete Worker |
pm_min_spare_servers | 2 | FPM-Pool: minimale idle Worker |
pm_max_spare_servers | 6 | FPM-Pool: maximale idle Worker |
memory_limit | 256M | PHP memory_limit pro Pool |
iteasapps::base::fail2ban — Jail-Schwellen
| Parameter | Default | Beschreibung |
|---|---|---|
auth_maxretry | 10 | Anzahl 401-Antworten bis zum Ban |
auth_findtime | 300 (5 min) | Zeitfenster für auth_maxretry |
auth_bantime | 3600 (1 h) | Dauer des Auth-Bans |
flood_maxretry | 50 | Anzahl 4xx-Antworten bis zum Ban |
flood_findtime | 60 (1 min) | Zeitfenster für flood_maxretry |
flood_bantime | 1800 (30 min) | Dauer des 4xx-Flood-Bans |
iteasapps::base::mariadb — Server-Defaults
| Parameter | Default | Beschreibung |
|---|---|---|
max_connections | 100 | Globales Connection-Limit |
bind_address | 0.0.0.0 | Bind-Adresse (für EDBS-Remote-Zugriff nötig) |
tls_cert_path | /etc/mysql/ssl/server-cert.pem | Vom Certbot-Renewal-Hook geschriebener Pfad |
tls_key_path | /etc/mysql/ssl/server-key.pem | Analog |
EDBS-Anbindung
Die EDBS-Anbindung ist als eigene Klasse iteas_services::edbs modelliert (im Schwester-Modul, weil sie eine Eigenschaft der MariaDB-Instanz auf dem Host beschreibt, nicht einer einzelnen App). Der MariaDB-User m01_edbs wird nicht von der App konsumiert, sondern vom externen EDBS-Server, der sich damit gegen die lokale MariaDB authentifiziert. Die Verbindung läuft also von außen herein, nicht aus der App heraus.
Die App liest die per EDBS importierten Datensätze über den lokalen App-Login. Quell-IP und Login-Name des EDBS-Servers tauchen daher nicht in den App-Klassen-Parametern auf.
Was die Klasse tut:
- Legt auf der MariaDB den Login
m01_edbs@<source_ip>an, mitREQUIRE SSLund aufSELECT/INSERT/UPDATEeingeschränkt. - Öffnet Port
3306/tcpper Firewall-Regel ausschließlich für die Quell-IP des EDBS-Servers. - Liest das Login-Passwort aus Vault (
secret/iteas_services/edbs/password).
Smart Class Parameters
| Parameter | Typ | Default | Beschreibung |
|---|---|---|---|
source_ip | String | — | Quell-IP des EDBS-Servers (Pflicht) |
target_database | String | — | Name der App-Datenbank, in die der EDBS-Server schreibt (z.B. iscan) |
sql_login | String | m01_edbs | MariaDB-User-Name |
Klassen-Ordering
Der GRANT auf target_database setzt voraus, dass die Datenbank zum Apply-Zeitpunkt existiert. Auf einem Host läuft iteas_services::edbs also nur sauber durch, wenn auch die App-Klasse klassifiziert ist, deren DB gemeint ist. Die Reihenfolge der Klassifizierung im Foreman ist egal — Puppet löst die Resource-Abhängigkeit im Katalog auf.
Vault-Integration
Secrets liegen in Vault statt in den Smart Class Parameters. Puppet liest sie während des Agent-Runs über das Hiera-Vault-Backend (siehe Foreman & Puppet Setup) und schreibt sie in eine lokale Datei auf dem Host. Die App kontaktiert Vault zur Laufzeit nicht — sie liest ausschließlich diese lokale Datei (siehe Applikation → Secrets-Handling).
Vault-Struktur (KV-v2, gemountet unter secret/):
Pro Klasse existiert ein einziges Vault-Secret als Hash, dessen Felder die einzelnen Werte tragen. Das entspricht der Konvention, die der Legacy-Stack bereits verwendet (lookup('iteasapps/native/iscan/config') → Hash). Die puppet-control-Hiera-Mounts liefern die Hierarchie:
secret/puppet/<env>/<certname>/ ← Host-spezifische Overrides (höchste Priorität)
secret/puppet/<env>/common/ ← Environment-weite Defaults
secret/puppet/common/ ← Global geltende DefaultsUnter jedem dieser Präfixe liegt dieselbe Struktur:
iteasapps/
├── iscan/
│ └── secrets ← Hash mit Feldern: db_password, cookie_validation_key,
│ edbs_db_password, deploy_token,
│ mailer_dsn (optional), sp_mailer_dsn (optional),
│ app_update_key (optional)
└── itransfer/
└── secrets ← Hash mit Feldern: db_password, cookie_validation_key,
deploy_token
iteas_services/
├── edbs/
│ └── secrets ← Hash mit Feld: password
└── geoip/
└── secrets ← Hash mit Feldern: account_id, license_keyPflicht vs. optional: Was im docker-compose keinen Default hatte, ist Pflicht — fehlt das Feld im Hash, scheitert der Lookup beim Zugriff. Felder mit Default sind optional; fehlen sie im Hash, greift der Default-Wert aus der Klasse.
Der edbs-Pfad liegt unter iteas_services/ statt iteasapps/<app>/, weil der dort hinterlegte User keine Eigenschaft einer App-Instanz ist, sondern eine MariaDB-Zugangsberechtigung auf Host-Ebene — dieselbe Trennung, die auch im Puppet-Code die Klasse iteas_services::edbs ins Schwester-Modul wandern lässt.
TLS-Material liegt nicht in Vault — siehe Webhost → TLS-Material.
Lookup-Key-Konvention
Pro Klasse gibt es einen Lookup, dessen Resultat ein Hash ist. Die einzelnen Werte liegen als Felder im Hash und werden in der Klasse per Schlüssel-Index extrahiert. Das ist die gleiche Konvention, die der Legacy-Stack bereits verwendet (lookup('iteasapps/native/iscan/config') → Hash).
| Lookup-Key (in Puppet) | Vault-Pfad (per Hiera-Mount aufgelöst, Beispiel) | Pflichtfelder |
|---|---|---|
iteasapps/iscan/secrets | secret/data/puppet/common/iteasapps/iscan/secrets | db_password, cookie_validation_key, edbs_db_password, deploy_token |
iteasapps/itransfer/secrets | secret/data/puppet/common/iteasapps/itransfer/secrets | db_password, cookie_validation_key, deploy_token |
iteas_services/edbs/secrets | secret/data/puppet/common/iteas_services/edbs/secrets | password |
geoip | secret/data/puppet/common/geoip | account_id, license_key (Host-übergreifend, da MaxMind-Account organisationsweit gilt) |
Aufgelöst wird das über den mounts-Block der Hiera-Konfiguration im puppet-control-Repo. Beispiel-Konfiguration:
mounts:
secret/data:
- "puppet/%{environment}/%{::trusted.certname}"
- "puppet/%{environment}/common"
- "puppet/common"lookup('iteasapps/iscan/secrets') wird damit in der gezeigten Reihenfolge gesucht (host-spezifisch → environment-weit → global), der erste Treffer gewinnt. Für ein Host-spezifisches Override eines einzelnen Wertes (z.B. eine eigene mailer_dsn nur für goessl.iscan.at) muss am Override-Pfad das gesamte Hash vorhanden sein — petems-hiera_vault macht keine Feld-für-Feld-Vermischung über mehrere Hierarchie-Ebenen.
Sensitive-Handling
Passwörter aus Vault werden in den Puppet-Klassen als Sensitive[String] typisiert und an Resources mit Sensitive-Parametern (password, password_hash etc.) weitergereicht. Puppet ersetzt den Wert im Catalog-Render durch die Sensitive-Maske; im Foreman-Report, in der PuppetDB und in den Agent-Logs erscheint nur diese Maske.
Ablauf einer Klassifizierung
Initialer Deployment-Lauf
- Cloud-Init registriert den Host beim Foreman und setzt Environment + Klasse(n).
- Admin trägt im Foreman die Pflicht-Parameter der klassifizierten Klassen ein.
- Admin legt die zugehörigen Secrets in Vault an (Pfad-Konvention siehe oben).
- Puppet-Agent kompiliert beim nächsten Run den Katalog und rollt aus:
iteasapps::base(einmalig), alle klassifizierten App-Klassen, abschließend der Smoke-Test pro App. - Bei erfolgreichem Smoke-Test ist die Applikation produktiv.
Weitere Applikation auf einem bestehenden Host
Sobald ein Host initialisiert ist, ist für jede weitere App kein Re-Provisioning nötig:
- Im Foreman die zusätzliche App-Klasse klassifizieren (z.B.
iteasapps::itransfernebeniteasapps::iscan). - Parameter und Vault-Secrets für die neue Klasse anlegen.
- Beim nächsten Agent-Run erkennt Puppet die neue Klasse; die Basis bleibt unverändert (idempotenter
include), neu ausgerollt werden nur die zusätzlichen App-Ressourcen.
Fehlerbehandlung
Fehlt ein Vault-Secret oder schlägt der Smoke-Test fehl, bricht der entsprechende Schritt im Puppet-Run ab. Der Rest des Katalogs läuft normal durch; der nächste Agent-Run versucht den Schritt erneut. Korrekturen erfolgen in den Smart-Class-Parametern oder in Vault — kein Code-Deploy, kein erneutes Provisioning.
