Skip to content

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

EndpointWert
Web-UI / APIhttps://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:

  1. Puppet-Agent installieren aus dem offiziellen Puppet-Repository (apt.puppet.com bzw. yum.puppet.com, je nach Distribution).
  2. 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).
  3. Environment iteasapps zuweisen über environment = iteasapps in /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:

  1. Per SSH auf die VM verbinden (Default-User und Key über das Proxmox-Cloud-Init-Modul).

  2. 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-agent

    Das . /etc/os-release liest den Distributions-Codenamen (z.B. bookworm, noble) aus der vom System selbst gepflegten os-release-Datei in $VERSION_CODENAME ein — 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:

    bash
    sudo rpm -Uvh "https://yum.puppet.com/puppet8-release-el-$(rpm -E '%{rhel}').noarch.rpm"
    sudo dnf install -y puppet-agent

    Analog liefert rpm -E '%{rhel}' die RHEL-Major-Version (8, 9) — auch hier kein manuelles Nachschlagen nötig.

  3. Agent konfigurieren und ersten Run anstoßen:

    bash
    sudo 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.service

    puppet agent --test schickt 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, dann systemctl enable --now. Ist der Service schon aktiv, wenn der manuelle Lauf startet, kollidieren beide am Agent-Lock und der manuelle Lauf bricht mit Another puppet instance is already running ab. Sollte der Service bereits laufen: sudo systemctl stop puppet davor, start danach.

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:

puppet-module-iteasapps

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-KlasseAufgabe
iteas_services::nginxNginx-Grundkonfiguration, globale Rate-Limit-Zonen, GeoIP-Map
iteas_services::php_fpmPHP 8.4 mit PHP-FPM (aus Ondrej-PPA), Standard-Module
iteas_services::mariadbMariaDB-Server mit TLS-Konfiguration
iteas_services::fail2banFail2Ban für statuscode-basierte IP-Bans
iteas_services::geoipMaxMind GeoIP2-Datenbank und geoipupdate

Applikations-Klassen

Pro deploybare Applikation existiert eine Klasse, die im Foreman direkt klassifiziert wird:

KlasseBeschreibung
iteasapps::iscanDeployt eine iScan-Instanz
iteasapps::itransferDeployt 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:

KlasseBeschreibung
iteas_services::edbsEDBS-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 TypeAufgabe
iteasapps::webhostSystem-User, FPM-Pool, Nginx-VHost, TLS-Zertifikat (Certbot)
iteasapps::releaseLädt das App-Tarball aus der GitLab Package Registry, deployt nach /var/www/<app>/, ruft Yii-Migrationen auf
iteasapps::healthcheckSmoke-Test der ausgerollten Applikation
iteas_services::sql::loginMariaDB-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

ParameterTypBeschreibung
domainStringFQDN der Instanz
edbs_db_userStringUsername für die Remote-EDBS-DB (outbound)
edbs_db_hostStringHost (bzw. host:port) der Remote-EDBS-DB
edbs_db_nameStringDatenbank-Name der Remote-EDBS-DB

Webhost / Infrastruktur

ParameterTypDefaultBeschreibung
system_userStringf01_iscanLinux-System-User
db_userStringiscanLokaler MariaDB-Login (= DB_USER) und gleichzeitig DB-Name (= DB_NAME)
db_hostnameStringlocalhostHostname, unter dem die App die DB anspricht (= DB_HOSTNAME)
allowed_countriesArray[String]['AT', 'DE', 'CH']Geoblocking-Whitelist
modeEnumnativenative (FPM-Pool) oder containerized (Docker-Container)
container_portOptional[Integer]Nur bei mode = containerized

App-Konfiguration (Yii / Customer / Mail)

ParameterTypDefaultEnv-Var
yii_environmentEnum['dev','prod']devYII_ENVIRONMENT
yii_debugBooleantrueYII_DEBUG
customerStringiteasCUSTOMER
sender_mail_addressStringnoreply@iscan.atSENDER_MAIL_ADDRESS
sp_sender_mail_addressStringnoreply@opst.atSP_SENDER_MAIL_ADDRESS
sp_user_invite_valid_hourInteger12SP_USER_INVITE_VALID_HOUR
enable_schema_cacheBooleantrueENABLE_SCHEMA_CACHE

Logging (Graylog & Loki)

ParameterTypDefaultEnv-Var
enable_loggingBooleanfalseENABLE_LOGGING
graylog_facilityStringiscan-devGRAYLOG_FACILITY
graylog_urlStringgelf.log.iteas.atGRAYLOG_URL
graylog_portInteger443GRAYLOG_PORT
loki_enable_loggingBooleanfalseLOKI_ENABLE_LOGGING
loki_api_urlStringhttps://api.log.iteas.cloud/loki/api/v1/pushLOKI_API_URL
loki_service_nameStringiscan-webLOKI_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-Secret edbs_db_password.
  • Inbound (iteas_services::edbs): Der externe EDBS-Server verbindet sich zur lokalen MariaDB. Credentials = MariaDB-User m01_edbs + Vault-Secret unter secret/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

ParameterDefaultBeschreibung
rate_general60r/mStandard-Limit für alle Routen
rate_api10r/sLimit für REST/API-Routen
rate_login5r/mLimit für Auth-Endpoints
burst_general20Burst-Tolerance für general
burst_api20Burst-Tolerance für api
burst_login3Burst-Tolerance für login
zone_size10mShared-Memory-Größe pro Zone
limit_req_status429Antwort-Code bei Rate-Limit-Verletzung

iteasapps::base::php — PHP-FPM

ParameterDefaultBeschreibung
php_version8.4PHP-Hauptversion (aus Ondrej-PPA)
php_modules['mysql','curl','mbstring','xml','intl']Pflicht-Module für die Iteas-Apps
pm_max_children20FPM-Pool: maximale Worker pro Pool
pm_start_servers4FPM-Pool: initial gestartete Worker
pm_min_spare_servers2FPM-Pool: minimale idle Worker
pm_max_spare_servers6FPM-Pool: maximale idle Worker
memory_limit256MPHP memory_limit pro Pool

iteasapps::base::fail2ban — Jail-Schwellen

ParameterDefaultBeschreibung
auth_maxretry10Anzahl 401-Antworten bis zum Ban
auth_findtime300 (5 min)Zeitfenster für auth_maxretry
auth_bantime3600 (1 h)Dauer des Auth-Bans
flood_maxretry50Anzahl 4xx-Antworten bis zum Ban
flood_findtime60 (1 min)Zeitfenster für flood_maxretry
flood_bantime1800 (30 min)Dauer des 4xx-Flood-Bans

iteasapps::base::mariadb — Server-Defaults

ParameterDefaultBeschreibung
max_connections100Globales Connection-Limit
bind_address0.0.0.0Bind-Adresse (für EDBS-Remote-Zugriff nötig)
tls_cert_path/etc/mysql/ssl/server-cert.pemVom Certbot-Renewal-Hook geschriebener Pfad
tls_key_path/etc/mysql/ssl/server-key.pemAnalog

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, mit REQUIRE SSL und auf SELECT/INSERT/UPDATE eingeschränkt.
  • Öffnet Port 3306/tcp per 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

ParameterTypDefaultBeschreibung
source_ipStringQuell-IP des EDBS-Servers (Pflicht)
target_databaseStringName der App-Datenbank, in die der EDBS-Server schreibt (z.B. iscan)
sql_loginStringm01_edbsMariaDB-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 Defaults

Unter 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_key

Pflicht 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/secretssecret/data/puppet/common/iteasapps/iscan/secretsdb_password, cookie_validation_key, edbs_db_password, deploy_token
iteasapps/itransfer/secretssecret/data/puppet/common/iteasapps/itransfer/secretsdb_password, cookie_validation_key, deploy_token
iteas_services/edbs/secretssecret/data/puppet/common/iteas_services/edbs/secretspassword
geoipsecret/data/puppet/common/geoipaccount_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:

yaml
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

  1. Cloud-Init registriert den Host beim Foreman und setzt Environment + Klasse(n).
  2. Admin trägt im Foreman die Pflicht-Parameter der klassifizierten Klassen ein.
  3. Admin legt die zugehörigen Secrets in Vault an (Pfad-Konvention siehe oben).
  4. 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.
  5. 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:

  1. Im Foreman die zusätzliche App-Klasse klassifizieren (z.B. iteasapps::itransfer neben iteasapps::iscan).
  2. Parameter und Vault-Secrets für die neue Klasse anlegen.
  3. 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.

Iteas Tools Integration Platform Version v1.0.21

Version: v1.0.21 Version: v1.0.21
Commit: 7a0e1c11
Deployed at: 2026-09-24T13:56:52Z