feat(PROJ-71): TLS-Pflicht (optional) für eingehenden SMTP-BCC-Kanal
fix: Audit-Log-Detailspalte per Tooltip statt hartem Truncate lesbar machen Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Sonnet 5
parent
e221fcf63f
commit
9db5eaa1e8
@@ -184,6 +184,7 @@ type SMTPConfig struct {
|
||||
Domain string `yaml:"domain"`
|
||||
TLSCert string `yaml:"tls_cert"`
|
||||
TLSKey string `yaml:"tls_key"`
|
||||
RequireTLS bool `yaml:"require_tls"` // PROJ-71: reject DATA over plaintext when true (needs tls_cert/tls_key set)
|
||||
MaxSizeMB int `yaml:"max_size_mb"`
|
||||
AllowedIPs []string `yaml:"allowed_ips"`
|
||||
TenantRouting string `yaml:"tenant_routing"` // "domain" or "default"
|
||||
|
||||
+2
-1
@@ -86,7 +86,8 @@
|
||||
| PROJ-68 | sudo-Provisionierung für Admin-Dienststeuerung fehlte komplett | Deployed | [PROJ-68](PROJ-68-sudo-provisionierung-dienststeuerung.md) | 2026-07-05 |
|
||||
| PROJ-69 | Admin-Dashboard Tab-Gruppierung (2-Ebenen-Navigation) | Deployed | [PROJ-69](PROJ-69-admin-tabs-gruppierung.md) | 2026-07-06 |
|
||||
| PROJ-70 | User-Self-Service IMAP-Rückholung (Archiv-Mail zurück ins Postfach) | Deployed | [PROJ-70](PROJ-70-imap-rueckholung-self-service.md) | 2026-07-07 |
|
||||
| PROJ-71 | TLS-Pflicht (optional) für eingehenden SMTP-BCC-Journaling-Kanal | Deployed | [PROJ-71](PROJ-71-smtp-require-tls.md) | 2026-07-08 |
|
||||
|
||||
<!-- Add features above this line -->
|
||||
|
||||
## Next Available ID: PROJ-71
|
||||
## Next Available ID: PROJ-72
|
||||
|
||||
@@ -0,0 +1,49 @@
|
||||
---
|
||||
id: PROJ-71
|
||||
title: TLS-Pflicht (optional) für eingehenden SMTP-BCC-Journaling-Kanal
|
||||
status: Deployed
|
||||
created: 2026-07-08
|
||||
---
|
||||
|
||||
## Problem
|
||||
|
||||
Eingehender SMTP-Daemon (`internal/smtpd/`) für BCC-Journaling erlaubt STARTTLS
|
||||
optional, erzwingt es aber nicht. Mails können unverschlüsselt per Klartext
|
||||
eintreffen, obwohl TLS-Zertifikat konfiguriert ist. Sicherheitslücke für den
|
||||
Journaling-Kanal, der Original-Mails 1:1 (Rohbyte-Erhalt für Revisionssicherheit,
|
||||
siehe GoBD-Anforderung) entgegennimmt.
|
||||
|
||||
## Lösung
|
||||
|
||||
Neues optionales Config-Flag `smtp.require_tls` (`config/config.go`, Feld
|
||||
`RequireTLS`). Wenn `true`:
|
||||
|
||||
- Start-Validierung: Daemon startet nicht, wenn `require_tls: true` gesetzt ist,
|
||||
aber `tls_cert`/`tls_key` fehlen (`internal/smtpd/smtpd.go` `Start()`).
|
||||
- Enforcement bei `MAIL FROM`: Session ohne aktive TLS-Verbindung wird mit
|
||||
`530 5.7.0 Must issue STARTTLS first` abgelehnt, bevor Daten übertragen werden
|
||||
(`session.Mail()`).
|
||||
- TLS-Status pro Session wird beim Verbindungsaufbau via
|
||||
`c.TLSConnectionState()` ermittelt (`backend.NewSession()`).
|
||||
|
||||
Standardmäßig deaktiviert (`require_tls` fehlt/false) — keine Breaking Change
|
||||
für bestehende Installationen ohne TLS-Zertifikat.
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
- `config/config.go`: `SMTPConfig.RequireTLS bool` `yaml:"require_tls"`.
|
||||
- `internal/smtpd/smtpd.go`:
|
||||
- `session.isTLS bool` Feld, gesetzt in `NewSession()`.
|
||||
- `Start()`: Fehler beim Boot, falls `RequireTLS` ohne Cert/Key.
|
||||
- `session.Mail()`: Reject vor Datenübertragung, spart Bandbreite ggü.
|
||||
Ablehnung erst nach `DATA`.
|
||||
- Rohbyte-Erhalt, Message-ID-Erhalt, Envelope/Header-Trennung (BCC-Journaling)
|
||||
bereits vorhanden und unverändert (siehe Code-Review vom 2026-07-08) — dieses
|
||||
Feature schließt nur die TLS-Lücke im Transportkanal.
|
||||
|
||||
## Acceptance Criteria
|
||||
|
||||
- [x] `require_tls: false`/fehlend → Verhalten unverändert (STARTTLS optional).
|
||||
- [x] `require_tls: true` ohne `tls_cert`/`tls_key` → Daemon-Start schlägt fehl.
|
||||
- [x] `require_tls: true` + Cert/Key gesetzt → Klartext-`MAIL FROM` wird mit
|
||||
530 abgelehnt, STARTTLS-Verbindung wird angenommen.
|
||||
+15
-1
@@ -225,6 +225,8 @@ func (d *Daemon) Start() error {
|
||||
return fmt.Errorf("smtpd: load TLS cert: %w", err)
|
||||
}
|
||||
srv.TLSConfig = &tls.Config{Certificates: []tls.Certificate{cert}}
|
||||
} else if d.cfg.RequireTLS {
|
||||
return fmt.Errorf("smtpd: require_tls is set but tls_cert/tls_key are missing")
|
||||
}
|
||||
|
||||
d.mu.Lock()
|
||||
@@ -322,16 +324,20 @@ func (b *backend) NewSession(c *smtp.Conn) (smtp.Session, error) {
|
||||
}
|
||||
}
|
||||
|
||||
b.daemon.logger.Debug("SMTP: new session", "ip", remoteIP)
|
||||
_, isTLS := c.TLSConnectionState()
|
||||
|
||||
b.daemon.logger.Debug("SMTP: new session", "ip", remoteIP, "tls", isTLS)
|
||||
return &session{
|
||||
daemon: b.daemon,
|
||||
remoteIP: remoteIP,
|
||||
isTLS: isTLS,
|
||||
}, nil
|
||||
}
|
||||
|
||||
type session struct {
|
||||
daemon *Daemon
|
||||
remoteIP string
|
||||
isTLS bool
|
||||
from string
|
||||
rcpts []string
|
||||
}
|
||||
@@ -342,6 +348,14 @@ func (s *session) AuthPlain(_, _ string) error {
|
||||
}
|
||||
|
||||
func (s *session) Mail(from string, _ *smtp.MailOptions) error {
|
||||
if s.daemon.cfg.RequireTLS && !s.isTLS {
|
||||
s.daemon.stats.Rejected.Add(1)
|
||||
return &smtp.SMTPError{
|
||||
Code: 530,
|
||||
EnhancedCode: smtp.EnhancedCode{5, 7, 0},
|
||||
Message: "Must issue STARTTLS first",
|
||||
}
|
||||
}
|
||||
s.from = from
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -13,6 +13,12 @@ import {
|
||||
TableHeader,
|
||||
TableRow,
|
||||
} from "@/components/ui/table";
|
||||
import {
|
||||
Tooltip,
|
||||
TooltipContent,
|
||||
TooltipProvider,
|
||||
TooltipTrigger,
|
||||
} from "@/components/ui/tooltip";
|
||||
|
||||
const AUDIT_PAGE_SIZE = 25;
|
||||
|
||||
@@ -75,8 +81,19 @@ export function AuditTab({
|
||||
</Badge>
|
||||
</TableCell>
|
||||
<TableCell>{entry.username}</TableCell>
|
||||
<TableCell className="max-w-xs truncate">
|
||||
<TableCell className="max-w-md">
|
||||
<TooltipProvider>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<span className="block truncate">
|
||||
{entry.detail}
|
||||
</span>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent className="max-w-md whitespace-pre-wrap break-all">
|
||||
{entry.detail}
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</TooltipProvider>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
|
||||
Reference in New Issue
Block a user