Fix DMARC guidance for send-only relays with optional rua= settings.
test / test (push) Has been cancelled

The panel now suggests policy-only DMARC by default, lets operators configure a default and per-domain report address, and DNS-checks hub _report._dmarc records. Future in-panel report ingestion is tracked as dmarc-reports in the roadmap.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-08-10 22:47:34 +03:00
parent 2bdc0ea9a8
commit efaf016c5f
25 changed files with 784 additions and 116 deletions
+9 -8
View File
@@ -12,9 +12,10 @@ var ErrNoAdmin = errors.New("no administrator account")
// Admin is the single panel administrator (security.md).
type Admin struct {
Username string
PasswordHash string
CreatedAt time.Time
Username string
PasswordHash string
DMARCReportEmail string // default rua= for all sending domains; empty = none
CreatedAt time.Time
}
// AdminExists reports whether the administrator account has been created. This
@@ -47,10 +48,10 @@ func (s *Store) CreateAdmin(username, passwordHash string) error {
// SASL logins applications authenticate with, which live in their own table.
// ErrNoAdmin is returned if setup has not happened yet, so a change can never
// silently create an account.
func (s *Store) UpdateAdmin(username, passwordHash string) error {
func (s *Store) UpdateAdmin(username, passwordHash, dmarcReportEmail string) error {
res, err := s.db.Exec(
"UPDATE admin SET username = ?, password_hash = ? WHERE id = 1",
username, passwordHash,
"UPDATE admin SET username = ?, password_hash = ?, dmarc_report_email = ? WHERE id = 1",
username, passwordHash, dmarcReportEmail,
)
if err != nil {
return fmt.Errorf("update admin: %w", err)
@@ -71,8 +72,8 @@ func (s *Store) GetAdmin() (Admin, error) {
a Admin
createdAt string
)
err := s.db.QueryRow("SELECT username, password_hash, created_at FROM admin WHERE id = 1").
Scan(&a.Username, &a.PasswordHash, &createdAt)
err := s.db.QueryRow("SELECT username, password_hash, dmarc_report_email, created_at FROM admin WHERE id = 1").
Scan(&a.Username, &a.PasswordHash, &a.DMARCReportEmail, &createdAt)
if errors.Is(err, sql.ErrNoRows) {
return Admin{}, ErrNoAdmin
}
+14 -3
View File
@@ -11,7 +11,7 @@ func TestUpdateAdmin(t *testing.T) {
if err := st.CreateAdmin("admin", "hash-one"); err != nil {
t.Fatalf("CreateAdmin: %v", err)
}
if err := st.UpdateAdmin("operator", "hash-two"); err != nil {
if err := st.UpdateAdmin("operator", "hash-two", "reports@hub.example"); err != nil {
t.Fatalf("UpdateAdmin: %v", err)
}
@@ -19,7 +19,18 @@ func TestUpdateAdmin(t *testing.T) {
if err != nil {
t.Fatalf("GetAdmin: %v", err)
}
if a.Username != "operator" || a.PasswordHash != "hash-two" {
if a.DMARCReportEmail != "reports@hub.example" {
t.Fatalf("dmarc email = %q", a.DMARCReportEmail)
}
if err := st.UpdateAdmin("operator", "hash-three", ""); err != nil {
t.Fatalf("clear dmarc email: %v", err)
}
a, err = st.GetAdmin()
if err != nil {
t.Fatalf("GetAdmin: %v", err)
}
if a.Username != "operator" || a.PasswordHash != "hash-three" {
t.Fatalf("unexpected admin after update: %+v", a)
}
if a.CreatedAt.IsZero() {
@@ -32,7 +43,7 @@ func TestUpdateAdmin(t *testing.T) {
func TestUpdateAdminWithoutAdmin(t *testing.T) {
st := openTestStore(t)
if err := st.UpdateAdmin("operator", "hash"); !errors.Is(err, ErrNoAdmin) {
if err := st.UpdateAdmin("operator", "hash", ""); !errors.Is(err, ErrNoAdmin) {
t.Fatalf("UpdateAdmin without admin = %v, want ErrNoAdmin", err)
}
exists, err := st.AdminExists()
+22 -3
View File
@@ -24,6 +24,7 @@ type Domain struct {
ID int64
Name string
DKIMSelector string
DMARCRua sql.NullString // NULL = inherit profile; Valid+empty = no reports
CreatedAt time.Time
AppCount int
}
@@ -54,7 +55,7 @@ func (s *Store) AddDomain(name, selector string) (Domain, error) {
// ordered by name.
func (s *Store) ListDomains() ([]Domain, error) {
rows, err := s.db.Query(`
SELECT d.id, d.name, d.dkim_selector, d.created_at,
SELECT d.id, d.name, d.dkim_selector, d.dmarc_rua, d.created_at,
(SELECT COUNT(*) FROM applications a WHERE a.domain_id = d.id)
FROM domains d
ORDER BY d.name`)
@@ -78,7 +79,7 @@ func (s *Store) ListDomains() ([]Domain, error) {
// ErrDomainNotFound.
func (s *Store) GetDomain(id int64) (Domain, error) {
row := s.db.QueryRow(`
SELECT d.id, d.name, d.dkim_selector, d.created_at,
SELECT d.id, d.name, d.dkim_selector, d.dmarc_rua, d.created_at,
(SELECT COUNT(*) FROM applications a WHERE a.domain_id = d.id)
FROM domains d
WHERE d.id = ?`, id)
@@ -120,7 +121,7 @@ func scanDomain(r scanRow) (Domain, error) {
d Domain
createdAt string
)
if err := r.Scan(&d.ID, &d.Name, &d.DKIMSelector, &createdAt, &d.AppCount); err != nil {
if err := r.Scan(&d.ID, &d.Name, &d.DKIMSelector, &d.DMARCRua, &createdAt, &d.AppCount); err != nil {
return Domain{}, err
}
d.CreatedAt, _ = time.Parse(time.RFC3339, createdAt)
@@ -137,3 +138,21 @@ func isUniqueViolation(err error) bool {
}
return false
}
// UpdateDomainDMARCRua sets how this domain resolves its DMARC rua= destination.
// NULL means inherit the administrator profile; Valid with an empty string means
// policy-only with no aggregate reports for this domain.
func (s *Store) UpdateDomainDMARCRua(id int64, rua sql.NullString) error {
res, err := s.db.Exec("UPDATE domains SET dmarc_rua = ? WHERE id = ?", rua, id)
if err != nil {
return fmt.Errorf("update domain dmarc rua: %w", err)
}
n, err := res.RowsAffected()
if err != nil {
return fmt.Errorf("update domain dmarc rua rows: %w", err)
}
if n == 0 {
return ErrDomainNotFound
}
return nil
}
+45
View File
@@ -0,0 +1,45 @@
package store
import (
"database/sql"
"testing"
)
func TestDomainDMARCRua(t *testing.T) {
st := openTestStore(t)
d, err := st.AddDomain("example.com", "sel")
if err != nil {
t.Fatalf("AddDomain: %v", err)
}
got, err := st.GetDomain(d.ID)
if err != nil {
t.Fatalf("GetDomain: %v", err)
}
if got.DMARCRua.Valid {
t.Fatal("new domain should inherit profile")
}
if err := st.UpdateDomainDMARCRua(d.ID, sql.NullString{Valid: true, String: "reports@hub.com"}); err != nil {
t.Fatalf("UpdateDomainDMARCRua custom: %v", err)
}
got, err = st.GetDomain(d.ID)
if err != nil || got.DMARCRua.String != "reports@hub.com" {
t.Fatalf("custom = %+v, err=%v", got.DMARCRua, err)
}
if err := st.UpdateDomainDMARCRua(d.ID, sql.NullString{Valid: true}); err != nil {
t.Fatalf("UpdateDomainDMARCRua none: %v", err)
}
got, _ = st.GetDomain(d.ID)
if !got.DMARCRua.Valid || got.DMARCRua.String != "" {
t.Fatalf("none = %+v", got.DMARCRua)
}
if err := st.UpdateDomainDMARCRua(d.ID, sql.NullString{}); err != nil {
t.Fatalf("UpdateDomainDMARCRua inherit: %v", err)
}
got, _ = st.GetDomain(d.ID)
if got.DMARCRua.Valid {
t.Fatalf("inherit = %+v", got.DMARCRua)
}
}
@@ -0,0 +1,5 @@
-- Optional DMARC aggregate-report destination (rua=) for the panel administrator
-- and per-domain overrides. NULL dmarc_rua on a domain means inherit the profile.
ALTER TABLE admin ADD COLUMN dmarc_report_email TEXT NOT NULL DEFAULT '';
ALTER TABLE domains ADD COLUMN dmarc_rua TEXT;