Ein kleiner REST Dienst (Mojolicious Lite), der Postfix Map Dateien pro Instanz verwaltet. Fokus ist Betriebssicherheit: atomare Writes, Backups mit Rotation, per Map Locking und optionaler Reload Status via systemd oder postmulti. Die API loescht keine Dateien auf dem Postfix Dateisystem. delmap entfernt nur Registrierungen in configs.json und liefert Hinweise zur manuellen Bereinigung.
- REST API fuer Map Dateien pro Instanz (lesen, schreiben, Restore aus Backup)
- Atomare Writes (Temp Datei, Rename)
- Timestamp Backups mit Rotation (Sortierung nach mtime, stabil)
- Per Map Locking (SH und EX) pro Instanz
- Optional
postmapnur fuerlmdboder andere definierte Typen (viapostmap_by_type) - Optionaler Reload und Status Check nach Aenderung (systemd oder postmulti)
- Access Control per CIDR Allowlist und Token Auth (Header
X-API-TokenoderAuthorization: Bearer) - Optional
require_httpsals Hardblock - Logging via Mojo::Log in Datei, Format bleibt kompatibel:
YYYY/MM/DD HH:MM:SS LEVEL Nachricht - JSON Pretty Canonical Writer fuer
configs.jsonUpdates
- Perl
- Mojolicious (Lite)
- Module im Script:
Mojo::*,Try::Tiny,Net::CIDR,Time::HiRes - Schreibrechte auf
map_dirpro Instanz, Backup Verzeichnisse, Logfile Pfad - Root oder ein dedizierter Service User, je nach Postfix Setup
.
├─ postfix-agent.pl
├─ global.json
├─ configs.json
├─ docs/
│ └─ banner.png
└─ systemd/
└─ postfix-agent.service
Token setzen und starten:
export API_TOKEN='change-me'
perl ./postfix-agent.pl daemonStandard Listen Adresse ist 0.0.0.0:5000, konfigurierbar in global.json.
Health Check:
curl -s http://127.0.0.1:5000/healthBeim Start erwartet das Script zwingend diese Dateien im app->home Verzeichnis:
global.jsonconfigs.json
Wenn eine fehlt, bricht der Start ab.
{
"secret": "change-this-long-random-secret-please",
"api_token": "set-a-token-here-or-use-env",
"listen": "0.0.0.0:5000",
"allowed_ips": ["127.0.0.1/32", "10.0.0.0/8"],
"logfile": "/var/log/mmbb/postfix-agent.log",
"serviceUser": "root",
"serviceGroup": "root",
"fileMode_service": "0644",
"fileMode_backup": "0660",
"tmpDir": "/tmp",
"lockDir": "/var/lock/postfix-agent",
"backupDir": "/var/backups/postfix-agent",
"ssl_enable": 0,
"ssl_cert_file": "/etc/ssl/certs/agent.crt",
"ssl_key_file": "/etc/ssl/private/agent.key",
"require_https": 0,
"dirs": {
"service_folder": ["backupDir", "lockDir"],
"service_mode": "0770"
}
}Wichtige Punkte:
API_TOKENist Pflicht. Entweder als ENVAPI_TOKENoderglobal.jsonFeldapi_token.allowed_ipsist eine CIDR Liste. Default faellt auf127.0.0.1, wenn nicht gesetzt.- Umask ist im Script restriktiv:
0007. Das passt gut fuer Group RW, Other none. require_httpsblockiert Requests, die nicht ueber HTTPS kommen. Das ist zusaetzlich zu TLS Listen.
Es gibt zwei moegliche Formen. Das Script akzeptiert beides.
Variante mit Wrapper instances:
{
"instances": {
"main": {
"map_dir": "/etc/postfix",
"config_dir": "/etc/postfix",
"backup_dir": "/var/backups/postfix-agent/main",
"lock_dir": "/var/lock/postfix-agent/main",
"max_backups": 5,
"reload_on_change": true,
"reload_cmd": "systemctl reload postfix",
"status_cmd": "systemctl status postfix --no-pager",
"globs": {
"virtual": "hash",
"*.lmdb": "lmdb",
"sender_access": "hash"
},
"postmap_by_type": {
"lmdb": "postmap -c {config_dir} lmdb:{path}",
"hash": "postmap -c {config_dir} hash:{path}"
}
}
}
}Variante flach ohne Wrapper:
{
"main": {
"map_dir": "/etc/postfix",
"config_dir": "/etc/postfix"
}
}globs steuert zwei Dinge:
- Welche Maps in
/instances/:inst/mapsangezeigt werden, wennallnicht gesetzt ist - Welcher
postmapTyp fuer eine Datei gilt. Es wird zuerst exaktes Match versucht, dann Pattern mit*.
Sicherheitsregel im Code:
main.cfundmaster.cfsind verboten und koennen nicht ueber die API gelesen oder geschrieben werden.
Jeder Request muss zwei Bedingungen erfuellen:
- IP muss in
allowed_ipsliegen (CIDR lookup) - Token muss stimmen, per Header:
X-API-Token: <token>
oderAuthorization: Bearer <token>
Sonst gibt es 403 Forbidden oder 401 Unauthorized.
CORS:
- Setzt
Access-Control-Allow-Origindynamisch aufOriginHeader - Erlaubt
GET, POST, DELETE, OPTIONS - Erlaubt Header
Content-Type, X-API-Token, Authorization
Info Endpoint.
Antwort:
{ "info": "Postfix Agent", "version": "1.5.1" }Prueft ob benoetigte Verzeichnisse existieren. Liefert ok.
Listet Instanzen aus configs.json.
Listet Maps.
- Ohne
all=1werden nur Dateien ermittelt, die durchglobsabgedeckt sind, ausserglobsist leer - Mit
all=1werden Dateien immap_dirgelesen (nur Files, keine Dotfiles)
Liest eine Map Datei als text/plain; charset=UTF-8.
Speichert Map Inhalt.
Input Wege:
Content-Type: application/jsonmit{ "content": "..." }- Form Param
content=... - Raw Body wird als UTF 8 dekodiert
Antwort enthält u a:
changedob Inhalt anders warbackupob Backup gemacht wurdepostmapErgebnis, falls ausgefuehrtreloadundstatus, fallsreload_on_changeaktiv und Kommandos gesetzt
Beispiel:
curl -s \
-H "X-API-Token: $API_TOKEN" \
-H "Content-Type: application/json" \
-d '{"content":"user@example.org OK\n"}' \
http://127.0.0.1:5000/instances/main/map/sender_accessListet Backups fuer eine Map. Sortiert nach mtime absteigend.
Liest ein bestimmtes Backup.
mode=textdefault, liefert Textmode=downloadliefert Octet Stream Attachmentmode=jsonliefert JSON mitcontentals String
Stellt ein Backup wieder her. Danach optional postmap, reload, status je nach Instanz Config.
Deregistriert eine Map nur in configs.json und liefert Hinweise.
Wichtig:
- Es wird keine Datei geloescht.
- Kein Reload wird ausgefuehrt.
Liest globs der Instanz aus configs.json.
Upsert fuer globs.
Payload Beispiele:
{ "map": "*.lmdb", "type": "lmdb" }oder mehrere:
{ "items": [ { "map": "virtual", "type": "hash" }, { "map": "*.lmdb", "type": "lmdb" } ] }Erlaubte Typen:
regexp, pcre, cidr, lmdb, hash, btree, db
Entfernt einen einzelnen Key aus globs.
[Unit]
Description=Postfix Map Agent REST
After=network.target
[Service]
Type=simple
WorkingDirectory=/opt/postfix-map-agent
Environment=API_TOKEN=change-me
ExecStart=/usr/bin/perl /opt/postfix-map-agent/postfix-agent.pl daemon
Restart=on-failure
User=root
Group=root
[Install]
WantedBy=multi-user.targetHinweise:
- Wenn du nicht als root laufen willst, muessen
map_dirund Backup Pfade passend berechtigt sein. - Umask ist im Script 0007. Das ist ok, wenn die Gruppe korrekt gesetzt ist.
- Setze
allowed_ipsrestriktiv und nutze am besten nur interne Netze oder localhost plus Reverse Proxy. - Token nicht in Logs oder Tickets kopieren.
- Wenn ueber Internet erreichbar, zwinge TLS. Entweder
ssl_enableplus cert key oder via Reverse Proxy undrequire_https=1. - Map Name Sanitizing blockiert Pfad Traversal und erlaubt nur
[0-9A-Za-z._-]. main.cfundmaster.cfsind gesperrt.
Token fehlt oder stimmt nicht. Header pruefen:
-H "X-API-Token: $API_TOKEN"oder:
-H "Authorization: Bearer $API_TOKEN"IP ist nicht in allowed_ips. Remote IP ist die TCP Quelle. Falls Reverse Proxy genutzt wird, brauchst du dort eine passende Network Policy, oder du erlaubst nur den Proxy und laesst den Proxy authentisieren.
Ein anderer Request haelt gerade den Lock. Lock Timeout ist 3 Sekunden.
In der JSON Antwort stehen rc und output. Damit siehst du direkt, was schief ging. Achte drauf, dass die Kommandos in postmap_by_type, reload_cmd, status_cmd korrekt sind.
MIT License. Siehe LICENSE.
