Loading admin/static/app.js +78 −1 Changes for admin/static/app.js: 78 added lines, 1 removed line. Original line number Diff line number Diff line Loading @@ -99,8 +99,37 @@ const ProxyPlusAdmin = (() => { } } async function refreshTlsProfiles() { const profiles = await api("GET", "/api/v1/tls/profiles"); const body = document.querySelector("#tls-profiles tbody"); body.innerHTML = ""; for (const p of profiles) { const tr = document.createElement("tr"); tr.innerHTML = `<td>${p.profile}</td><td>${(p.domains || []).join(", ")}</td><td>${p.acme_enabled ? "ja" : "nein"}</td><td>${p.acme_email}</td><td>${p.ca_plugin}</td>`; body.appendChild(tr); } } async function refreshDkimKeys() { const keys = await api("GET", "/api/v1/security/dkim-keys"); const body = document.querySelector("#dkim-keys tbody"); body.innerHTML = ""; for (const k of keys) { const tr = document.createElement("tr"); tr.innerHTML = `<td>${k.domain}</td><td>${k.selector}</td><td>${k.key_path}</td><td>${k.enabled ? "ja" : "nein"}</td>`; body.appendChild(tr); } } async function refreshAll() { await Promise.all([refreshStatus(), refreshRoutes(), refreshRelays(), refreshPools()]); await Promise.all([ refreshStatus(), refreshRoutes(), refreshRelays(), refreshPools(), refreshTlsProfiles(), refreshDkimKeys(), ]); } function showNewRouteForm() { Loading Loading @@ -143,6 +172,50 @@ const ProxyPlusAdmin = (() => { } } function showNewTlsProfileForm() { document.getElementById("new-tls-profile-form").hidden = false; } async function createTlsProfile() { document.getElementById("tls-profile-error").textContent = ""; try { const domains = document.getElementById("tls-profile-domains").value .split(",") .map((s) => s.trim()) .filter(Boolean); await api("POST", "/api/v1/tls/profiles", { profile: document.getElementById("tls-profile-id").value, domains, acme_enabled: document.getElementById("tls-profile-acme").checked, acme_email: document.getElementById("tls-profile-email").value, acme_directory_url: document.getElementById("tls-profile-directory").value, ca_plugin: document.getElementById("tls-profile-ca-plugin").value, }); await refreshTlsProfiles(); } catch (e) { document.getElementById("tls-profile-error").textContent = e.message; } } function showNewDkimKeyForm() { document.getElementById("new-dkim-key-form").hidden = false; } async function createDkimKey() { document.getElementById("dkim-key-error").textContent = ""; try { await api("POST", "/api/v1/security/dkim-keys", { domain: document.getElementById("dkim-domain").value, selector: document.getElementById("dkim-selector").value, key_path: document.getElementById("dkim-keypath").value, enabled: document.getElementById("dkim-enabled").checked, }); await refreshDkimKeys(); } catch (e) { document.getElementById("dkim-key-error").textContent = e.message; } } function showNewPoolForm() { document.getElementById("new-pool-form").hidden = false; } Loading Loading @@ -186,6 +259,10 @@ const ProxyPlusAdmin = (() => { createRoute, showNewRelayForm, createRelay, showNewTlsProfileForm, createTlsProfile, showNewDkimKeyForm, createDkimKey, showNewPoolForm, createPool, }; Loading admin/static/index.html +30 −0 Changes for admin/static/index.html: 30 added lines, 0 removed lines. Original line number Diff line number Diff line Loading @@ -49,6 +49,36 @@ <div id="relay-error" class="error"></div> </div> <h2>TLS-Profile / Zertifikate</h2> <table id="tls-profiles"><thead><tr><th>Profil</th><th>Domains</th><th>ACME</th><th>E-Mail</th><th>CA-Plugin</th></tr></thead><tbody></tbody></table> <button onclick="ProxyPlusAdmin.showNewTlsProfileForm()">Neues Profil</button> <div id="new-tls-profile-form" hidden> <input id="tls-profile-id" placeholder="Profil-ID, z.B. www"> <input id="tls-profile-domains" placeholder="Domains, kommagetrennt, erste = Subject CN"> <label><input type="checkbox" id="tls-profile-acme"> ACME aktiv (Let's Encrypt/ZeroSSL)</label> <input id="tls-profile-email" placeholder="ACME E-Mail (bei ACME aktiv erforderlich)"> <input id="tls-profile-directory" placeholder="ACME Directory-URL (leer = Plugin-Standard)"> <select id="tls-profile-ca-plugin"> <option value="">(automatisch, nur bei genau einem geladenen Plugin)</option> <option value="letsencrypt">letsencrypt</option> <option value="zerossl">zerossl</option> </select> <button onclick="ProxyPlusAdmin.createTlsProfile()">Anlegen</button> <div id="tls-profile-error" class="error"></div> </div> <h2>DKIM-Schluessel</h2> <table id="dkim-keys"><thead><tr><th>Domain</th><th>Selector</th><th>Key-Pfad</th><th>Aktiv</th></tr></thead><tbody></tbody></table> <button onclick="ProxyPlusAdmin.showNewDkimKeyForm()">Neuer Schluessel</button> <div id="new-dkim-key-form" hidden> <input id="dkim-domain" placeholder="Signierende Domain, z.B. example.com"> <input id="dkim-selector" placeholder="Selector, z.B. default"> <input id="dkim-keypath" placeholder="Pfad zum PEM-Private-Key"> <label><input type="checkbox" id="dkim-enabled" checked> Aktiv</label> <button onclick="ProxyPlusAdmin.createDkimKey()">Anlegen</button> <div id="dkim-key-error" class="error"></div> </div> <h2>Load-Balancer-Pools</h2> <table id="lb-pools"><thead><tr><th>ID</th><th>Art</th><th>Algorithmus</th><th>Ziele (Status)</th></tr></thead><tbody></tbody></table> <button onclick="ProxyPlusAdmin.showNewPoolForm()">Neuer Pool</button> Loading docs/ARCHITECTURE.md +34 −9 Changes for docs/ARCHITECTURE.md: 34 added lines, 9 removed lines. Original line number Diff line number Diff line Loading @@ -130,8 +130,30 @@ Real und einsatzbereit: `SmtpD`/`SmtpEvent` (Server), `SmtpClient` (Relay-Client vorhanden. **Lücke:** nur `AUTH_PLAIN`/`AUTH_LOGIN` (kein CRAM-MD5 — der Auftrag sagt selbst "sofern unterstützt", also entfällt es explizit). **Kein DKIM/Filter-Hook** — `DataEvent()` liefert ein `const SmtpEnvelope&`; zum Signieren/Modifizieren baut ProxyPlus für den Relay-Schritt eine **eigene** `SmtpEnvelope` für den ausgehenden `SmtpClient`, in die z. B. ein DKIM-Signatur-Header vor dem restlichen Body eingefügt wird. Relay-Schritt eine **eigene** `SmtpEnvelope` für den ausgehenden `SmtpClient`, in die ein DKIM-Signatur-Header vor dem restlichen Body eingefügt wird (siehe §6c). ## 6c. DKIM/DMARC-Verdrahtung (2026-08-07) `SmtpProxyController::DataEvent` (`src/smtp/SmtpProxyServer.cpp`) verdrahtet `DkimAdapter`/`DmarcAdapter` (§8) an zwei Stellen: - **Ausgehend (Signieren).** Ein neuer Record-Typ `DkimKeyRecord` (`src/store/Records.h`, `RecordTable::DkimKey`) ordnet einer signierenden Domain Selector + PEM-Key-Pfad zu, verwaltet über `GET/POST /api/v1/security/dkim-keys` (`security.dkim.read`/`security.dkim.write`, siehe `ROLES.md`) und die Admin-GUI. Trägt die MAIL-FROM-Domain einer Nachricht einen passenden, aktivierten Schlüssel, signiert `DataEvent` sie vor der Weiterleitung — außer sie trägt bereits eine `DKIM-Signature` (kein Doppel-Signieren beim reinen Durchreichen). - **Eingehend (Verifizieren) — nur beratend.** Trägt eine durchgereichte Nachricht bereits eine `DKIM-Signature`, verifiziert `DataEvent` sie (`DkimAdapter::verify()` holt den Public Key selbst per DNS, braucht keine Konfiguration) und wertet das Ergebnis per `DmarcAdapter` gegen die `_dmarc.<domain>`-Policy aus. Das Ergebnis wird als Standard-`Authentication-Results`- Header (RFC 8601) angehängt, **nicht** zum Zurückweisen/Quarantänisieren verwendet: ProxyPlus ist an dieser Stelle ein Relay-Hop, kein finaler Zustellagent, und die Auswertung hat zwei bekannte Lücken (kein SPF — `spfPass` ist immer `false`, siehe §8; `fromDomain` wird aus der Umschlag-MAIL-FROM-Adresse angenähert statt aus einem geparsten RFC5322.From-Header). Eine spätere Durchsetzung (Reject/Quarantäne nach Policy) bräuchte zuerst eine echte SPF-Implementierung und RFC5322.From-Parsing, um keine legitime Post fälschlich abzulehnen. ## 6a. Let's Encrypt / ZeroSSL / ACME (TLS-Zertifikatsbeschaffung) Loading Loading @@ -278,13 +300,16 @@ proxyplus/ Split-Brain-Schutz bräuchte es eine Membership-/Write-Ordering-Schicht. - DkimAdapter/DmarcAdapter sind eine eigene RFC-6376/7489-Implementierung (kein libopendkim/libopendmarc mehr — deren Header hatten je nach System unterschiedliche Include-Layouts und dkim.h ist ohne Weiteres nicht C++-kompatibel, `_Bool`). Bekannte Lücken gegenüber dem vollen RFC: kein Fallback auf die Organisationsdomain bei DMARC (RFC 7489 §6.6.3), relaxed-Alignment approximiert einen Public-Suffix-List-Abgleich nur über einen Suffix-Vergleich, `pct=` wird geparst aber nicht gesampelt (Policy wird immer vollständig angewendet), SPF-Alignment vertraut dem `spfPass`-Flag des Callers ohne eigenen Domain-Vergleich (das Interface bekommt keine separate SPF-authentifizierte Domain). Include-Layouts und dkim.h ist ohne Weiteres nicht C++-kompatibel, `_Bool`). Seit 2026-08-07 in `SmtpProxyServer::DataEvent` verdrahtet (§6c) — ausgehendes Signieren pro konfigurierter Domain, eingehendes Verifizieren nur beratend (`Authentication-Results`-Header, keine Durchsetzung). Bekannte Lücken gegenüber dem vollen RFC: kein Fallback auf die Organisationsdomain bei DMARC (RFC 7489 §6.6.3), relaxed-Alignment approximiert einen Public-Suffix-List-Abgleich nur über einen Suffix-Vergleich, `pct=` wird geparst aber nicht gesampelt (Policy wird immer vollständig angewendet), **keine SPF-Implementierung** — `spfPass` wird von `DataEvent` deshalb immer als `false` übergeben, SPF-Alignment kann also nie zum DMARC-Pass beitragen (nur DKIM-Alignment kann), und `fromDomain` ist die Umschlag-MAIL-FROM-Domain, kein geparster RFC5322.From-Header. - Gruppen-Anlage (`addGroup`) fehlt aktuell im `authdb::client`-API (nur `listGroups()` lesend, `GPOadd` schreibend) — `scripts/authdb-bootstrap.sh` dokumentiert das explizit als authdb-seitige Lücke statt sie zu umgehen. docs/ROLES.md +5 −0 Changes for docs/ROLES.md: 5 added lines, 0 removed lines. Original line number Diff line number Diff line Loading @@ -60,6 +60,11 @@ load balancer section) -- pools are shared between HTTP routes and SMTP relay ru TLS profiles they get their own resource class rather than living under `http.*`/`smtp.*`: `lb.pools.read`, `lb.pools.write`. Plus two more for managing per-domain DKIM signing keys (`/api/v1/security/dkim-keys`, see `ARCHITECTURE.md` §6c) -- grouped under `security.*` alongside `smtp.quarantine.manage` rather than `smtp.*` since it's `proxyplus.security_admin`'s resource, not `proxyplus.smtp_admin`'s: `security.dkim.read`, `security.dkim.write`. `scripts/authdb-bootstrap.sh` legt für jede dieser Berechtigungen eine GPO an und trägt die Standardrollen-Gruppen als `allowed`-Mitglieder ein (z. B. `proxyplus.super_admin` in allen GPOs, `proxyplus.smtp_admin` in allen `smtp.*`-GPOs). Loading src/api/JsonApiServer.cpp +36 −0 Changes for src/api/JsonApiServer.cpp: 36 added lines, 0 removed lines. Original line number Diff line number Diff line Loading @@ -403,6 +403,42 @@ void JsonApiController::registerRoutes() { json_object_object_add(resp, "status", json_object_new_string("created")); return json_object_to_json_string(resp); }}); // Per-domain DKIM signing keys, consulted by SmtpProxyServer::DataEvent for outbound mail // (see docs/ARCHITECTURE.md §6). Verifying an *incoming* signature needs no config here -- // DkimAdapter::verify() resolves the signer's public key via DNS itself. _routes.push_back( {"GET", "/api/v1/security/dkim-keys", "security.dkim.read", [this](RouteContext &) -> std::string { JsonGuard arr(json_object_new_array()); for (const auto &k : _configStore.listDkimKeys()) { JsonGuard item(json_object_new_object()); json_object_object_add(item, "domain", json_object_new_string(k.domain.c_str())); json_object_object_add(item, "selector", json_object_new_string(k.selector.c_str())); json_object_object_add(item, "key_path", json_object_new_string(k.keyPath.c_str())); json_object_object_add(item, "enabled", json_object_new_boolean(k.enabled)); json_object_get(item.obj); json_object_array_add(arr.obj, item.obj); } return json_object_to_json_string(arr); }}); _routes.push_back( {"POST", "/api/v1/security/dkim-keys", "security.dkim.write", [this](RouteContext &ctx) -> std::string { JsonGuard body(json_tokener_parse(ctx.body.c_str())); if (!body.obj) return jsonError("invalid JSON body"); store::DkimKeyRecord rec; rec.domain = getStringField(body, "domain"); rec.selector = getStringField(body, "selector"); rec.keyPath = getStringField(body, "key_path"); rec.enabled = getBoolField(body, "enabled", true); if (rec.domain.empty() || rec.selector.empty() || rec.keyPath.empty()) { return jsonError("domain, selector and key_path are required"); } _configStore.putDkimKey(rec); JsonGuard resp(json_object_new_object()); json_object_object_add(resp, "status", json_object_new_string("created")); return json_object_to_json_string(resp); }}); } namespace { Loading Loading
admin/static/app.js +78 −1 Changes for admin/static/app.js: 78 added lines, 1 removed line. Original line number Diff line number Diff line Loading @@ -99,8 +99,37 @@ const ProxyPlusAdmin = (() => { } } async function refreshTlsProfiles() { const profiles = await api("GET", "/api/v1/tls/profiles"); const body = document.querySelector("#tls-profiles tbody"); body.innerHTML = ""; for (const p of profiles) { const tr = document.createElement("tr"); tr.innerHTML = `<td>${p.profile}</td><td>${(p.domains || []).join(", ")}</td><td>${p.acme_enabled ? "ja" : "nein"}</td><td>${p.acme_email}</td><td>${p.ca_plugin}</td>`; body.appendChild(tr); } } async function refreshDkimKeys() { const keys = await api("GET", "/api/v1/security/dkim-keys"); const body = document.querySelector("#dkim-keys tbody"); body.innerHTML = ""; for (const k of keys) { const tr = document.createElement("tr"); tr.innerHTML = `<td>${k.domain}</td><td>${k.selector}</td><td>${k.key_path}</td><td>${k.enabled ? "ja" : "nein"}</td>`; body.appendChild(tr); } } async function refreshAll() { await Promise.all([refreshStatus(), refreshRoutes(), refreshRelays(), refreshPools()]); await Promise.all([ refreshStatus(), refreshRoutes(), refreshRelays(), refreshPools(), refreshTlsProfiles(), refreshDkimKeys(), ]); } function showNewRouteForm() { Loading Loading @@ -143,6 +172,50 @@ const ProxyPlusAdmin = (() => { } } function showNewTlsProfileForm() { document.getElementById("new-tls-profile-form").hidden = false; } async function createTlsProfile() { document.getElementById("tls-profile-error").textContent = ""; try { const domains = document.getElementById("tls-profile-domains").value .split(",") .map((s) => s.trim()) .filter(Boolean); await api("POST", "/api/v1/tls/profiles", { profile: document.getElementById("tls-profile-id").value, domains, acme_enabled: document.getElementById("tls-profile-acme").checked, acme_email: document.getElementById("tls-profile-email").value, acme_directory_url: document.getElementById("tls-profile-directory").value, ca_plugin: document.getElementById("tls-profile-ca-plugin").value, }); await refreshTlsProfiles(); } catch (e) { document.getElementById("tls-profile-error").textContent = e.message; } } function showNewDkimKeyForm() { document.getElementById("new-dkim-key-form").hidden = false; } async function createDkimKey() { document.getElementById("dkim-key-error").textContent = ""; try { await api("POST", "/api/v1/security/dkim-keys", { domain: document.getElementById("dkim-domain").value, selector: document.getElementById("dkim-selector").value, key_path: document.getElementById("dkim-keypath").value, enabled: document.getElementById("dkim-enabled").checked, }); await refreshDkimKeys(); } catch (e) { document.getElementById("dkim-key-error").textContent = e.message; } } function showNewPoolForm() { document.getElementById("new-pool-form").hidden = false; } Loading Loading @@ -186,6 +259,10 @@ const ProxyPlusAdmin = (() => { createRoute, showNewRelayForm, createRelay, showNewTlsProfileForm, createTlsProfile, showNewDkimKeyForm, createDkimKey, showNewPoolForm, createPool, }; Loading
admin/static/index.html +30 −0 Changes for admin/static/index.html: 30 added lines, 0 removed lines. Original line number Diff line number Diff line Loading @@ -49,6 +49,36 @@ <div id="relay-error" class="error"></div> </div> <h2>TLS-Profile / Zertifikate</h2> <table id="tls-profiles"><thead><tr><th>Profil</th><th>Domains</th><th>ACME</th><th>E-Mail</th><th>CA-Plugin</th></tr></thead><tbody></tbody></table> <button onclick="ProxyPlusAdmin.showNewTlsProfileForm()">Neues Profil</button> <div id="new-tls-profile-form" hidden> <input id="tls-profile-id" placeholder="Profil-ID, z.B. www"> <input id="tls-profile-domains" placeholder="Domains, kommagetrennt, erste = Subject CN"> <label><input type="checkbox" id="tls-profile-acme"> ACME aktiv (Let's Encrypt/ZeroSSL)</label> <input id="tls-profile-email" placeholder="ACME E-Mail (bei ACME aktiv erforderlich)"> <input id="tls-profile-directory" placeholder="ACME Directory-URL (leer = Plugin-Standard)"> <select id="tls-profile-ca-plugin"> <option value="">(automatisch, nur bei genau einem geladenen Plugin)</option> <option value="letsencrypt">letsencrypt</option> <option value="zerossl">zerossl</option> </select> <button onclick="ProxyPlusAdmin.createTlsProfile()">Anlegen</button> <div id="tls-profile-error" class="error"></div> </div> <h2>DKIM-Schluessel</h2> <table id="dkim-keys"><thead><tr><th>Domain</th><th>Selector</th><th>Key-Pfad</th><th>Aktiv</th></tr></thead><tbody></tbody></table> <button onclick="ProxyPlusAdmin.showNewDkimKeyForm()">Neuer Schluessel</button> <div id="new-dkim-key-form" hidden> <input id="dkim-domain" placeholder="Signierende Domain, z.B. example.com"> <input id="dkim-selector" placeholder="Selector, z.B. default"> <input id="dkim-keypath" placeholder="Pfad zum PEM-Private-Key"> <label><input type="checkbox" id="dkim-enabled" checked> Aktiv</label> <button onclick="ProxyPlusAdmin.createDkimKey()">Anlegen</button> <div id="dkim-key-error" class="error"></div> </div> <h2>Load-Balancer-Pools</h2> <table id="lb-pools"><thead><tr><th>ID</th><th>Art</th><th>Algorithmus</th><th>Ziele (Status)</th></tr></thead><tbody></tbody></table> <button onclick="ProxyPlusAdmin.showNewPoolForm()">Neuer Pool</button> Loading
docs/ARCHITECTURE.md +34 −9 Changes for docs/ARCHITECTURE.md: 34 added lines, 9 removed lines. Original line number Diff line number Diff line Loading @@ -130,8 +130,30 @@ Real und einsatzbereit: `SmtpD`/`SmtpEvent` (Server), `SmtpClient` (Relay-Client vorhanden. **Lücke:** nur `AUTH_PLAIN`/`AUTH_LOGIN` (kein CRAM-MD5 — der Auftrag sagt selbst "sofern unterstützt", also entfällt es explizit). **Kein DKIM/Filter-Hook** — `DataEvent()` liefert ein `const SmtpEnvelope&`; zum Signieren/Modifizieren baut ProxyPlus für den Relay-Schritt eine **eigene** `SmtpEnvelope` für den ausgehenden `SmtpClient`, in die z. B. ein DKIM-Signatur-Header vor dem restlichen Body eingefügt wird. Relay-Schritt eine **eigene** `SmtpEnvelope` für den ausgehenden `SmtpClient`, in die ein DKIM-Signatur-Header vor dem restlichen Body eingefügt wird (siehe §6c). ## 6c. DKIM/DMARC-Verdrahtung (2026-08-07) `SmtpProxyController::DataEvent` (`src/smtp/SmtpProxyServer.cpp`) verdrahtet `DkimAdapter`/`DmarcAdapter` (§8) an zwei Stellen: - **Ausgehend (Signieren).** Ein neuer Record-Typ `DkimKeyRecord` (`src/store/Records.h`, `RecordTable::DkimKey`) ordnet einer signierenden Domain Selector + PEM-Key-Pfad zu, verwaltet über `GET/POST /api/v1/security/dkim-keys` (`security.dkim.read`/`security.dkim.write`, siehe `ROLES.md`) und die Admin-GUI. Trägt die MAIL-FROM-Domain einer Nachricht einen passenden, aktivierten Schlüssel, signiert `DataEvent` sie vor der Weiterleitung — außer sie trägt bereits eine `DKIM-Signature` (kein Doppel-Signieren beim reinen Durchreichen). - **Eingehend (Verifizieren) — nur beratend.** Trägt eine durchgereichte Nachricht bereits eine `DKIM-Signature`, verifiziert `DataEvent` sie (`DkimAdapter::verify()` holt den Public Key selbst per DNS, braucht keine Konfiguration) und wertet das Ergebnis per `DmarcAdapter` gegen die `_dmarc.<domain>`-Policy aus. Das Ergebnis wird als Standard-`Authentication-Results`- Header (RFC 8601) angehängt, **nicht** zum Zurückweisen/Quarantänisieren verwendet: ProxyPlus ist an dieser Stelle ein Relay-Hop, kein finaler Zustellagent, und die Auswertung hat zwei bekannte Lücken (kein SPF — `spfPass` ist immer `false`, siehe §8; `fromDomain` wird aus der Umschlag-MAIL-FROM-Adresse angenähert statt aus einem geparsten RFC5322.From-Header). Eine spätere Durchsetzung (Reject/Quarantäne nach Policy) bräuchte zuerst eine echte SPF-Implementierung und RFC5322.From-Parsing, um keine legitime Post fälschlich abzulehnen. ## 6a. Let's Encrypt / ZeroSSL / ACME (TLS-Zertifikatsbeschaffung) Loading Loading @@ -278,13 +300,16 @@ proxyplus/ Split-Brain-Schutz bräuchte es eine Membership-/Write-Ordering-Schicht. - DkimAdapter/DmarcAdapter sind eine eigene RFC-6376/7489-Implementierung (kein libopendkim/libopendmarc mehr — deren Header hatten je nach System unterschiedliche Include-Layouts und dkim.h ist ohne Weiteres nicht C++-kompatibel, `_Bool`). Bekannte Lücken gegenüber dem vollen RFC: kein Fallback auf die Organisationsdomain bei DMARC (RFC 7489 §6.6.3), relaxed-Alignment approximiert einen Public-Suffix-List-Abgleich nur über einen Suffix-Vergleich, `pct=` wird geparst aber nicht gesampelt (Policy wird immer vollständig angewendet), SPF-Alignment vertraut dem `spfPass`-Flag des Callers ohne eigenen Domain-Vergleich (das Interface bekommt keine separate SPF-authentifizierte Domain). Include-Layouts und dkim.h ist ohne Weiteres nicht C++-kompatibel, `_Bool`). Seit 2026-08-07 in `SmtpProxyServer::DataEvent` verdrahtet (§6c) — ausgehendes Signieren pro konfigurierter Domain, eingehendes Verifizieren nur beratend (`Authentication-Results`-Header, keine Durchsetzung). Bekannte Lücken gegenüber dem vollen RFC: kein Fallback auf die Organisationsdomain bei DMARC (RFC 7489 §6.6.3), relaxed-Alignment approximiert einen Public-Suffix-List-Abgleich nur über einen Suffix-Vergleich, `pct=` wird geparst aber nicht gesampelt (Policy wird immer vollständig angewendet), **keine SPF-Implementierung** — `spfPass` wird von `DataEvent` deshalb immer als `false` übergeben, SPF-Alignment kann also nie zum DMARC-Pass beitragen (nur DKIM-Alignment kann), und `fromDomain` ist die Umschlag-MAIL-FROM-Domain, kein geparster RFC5322.From-Header. - Gruppen-Anlage (`addGroup`) fehlt aktuell im `authdb::client`-API (nur `listGroups()` lesend, `GPOadd` schreibend) — `scripts/authdb-bootstrap.sh` dokumentiert das explizit als authdb-seitige Lücke statt sie zu umgehen.
docs/ROLES.md +5 −0 Changes for docs/ROLES.md: 5 added lines, 0 removed lines. Original line number Diff line number Diff line Loading @@ -60,6 +60,11 @@ load balancer section) -- pools are shared between HTTP routes and SMTP relay ru TLS profiles they get their own resource class rather than living under `http.*`/`smtp.*`: `lb.pools.read`, `lb.pools.write`. Plus two more for managing per-domain DKIM signing keys (`/api/v1/security/dkim-keys`, see `ARCHITECTURE.md` §6c) -- grouped under `security.*` alongside `smtp.quarantine.manage` rather than `smtp.*` since it's `proxyplus.security_admin`'s resource, not `proxyplus.smtp_admin`'s: `security.dkim.read`, `security.dkim.write`. `scripts/authdb-bootstrap.sh` legt für jede dieser Berechtigungen eine GPO an und trägt die Standardrollen-Gruppen als `allowed`-Mitglieder ein (z. B. `proxyplus.super_admin` in allen GPOs, `proxyplus.smtp_admin` in allen `smtp.*`-GPOs). Loading
src/api/JsonApiServer.cpp +36 −0 Changes for src/api/JsonApiServer.cpp: 36 added lines, 0 removed lines. Original line number Diff line number Diff line Loading @@ -403,6 +403,42 @@ void JsonApiController::registerRoutes() { json_object_object_add(resp, "status", json_object_new_string("created")); return json_object_to_json_string(resp); }}); // Per-domain DKIM signing keys, consulted by SmtpProxyServer::DataEvent for outbound mail // (see docs/ARCHITECTURE.md §6). Verifying an *incoming* signature needs no config here -- // DkimAdapter::verify() resolves the signer's public key via DNS itself. _routes.push_back( {"GET", "/api/v1/security/dkim-keys", "security.dkim.read", [this](RouteContext &) -> std::string { JsonGuard arr(json_object_new_array()); for (const auto &k : _configStore.listDkimKeys()) { JsonGuard item(json_object_new_object()); json_object_object_add(item, "domain", json_object_new_string(k.domain.c_str())); json_object_object_add(item, "selector", json_object_new_string(k.selector.c_str())); json_object_object_add(item, "key_path", json_object_new_string(k.keyPath.c_str())); json_object_object_add(item, "enabled", json_object_new_boolean(k.enabled)); json_object_get(item.obj); json_object_array_add(arr.obj, item.obj); } return json_object_to_json_string(arr); }}); _routes.push_back( {"POST", "/api/v1/security/dkim-keys", "security.dkim.write", [this](RouteContext &ctx) -> std::string { JsonGuard body(json_tokener_parse(ctx.body.c_str())); if (!body.obj) return jsonError("invalid JSON body"); store::DkimKeyRecord rec; rec.domain = getStringField(body, "domain"); rec.selector = getStringField(body, "selector"); rec.keyPath = getStringField(body, "key_path"); rec.enabled = getBoolField(body, "enabled", true); if (rec.domain.empty() || rec.selector.empty() || rec.keyPath.empty()) { return jsonError("domain, selector and key_path are required"); } _configStore.putDkimKey(rec); JsonGuard resp(json_object_new_object()); json_object_object_add(resp, "status", json_object_new_string("created")); return json_object_to_json_string(resp); }}); } namespace { Loading