Repository navigation
Roadmap
Rough direction, not promises.
Each domain's own page shows whether its DMARC pass rate is trending up or down: recent days compared with the period before. A domain trending down gets an alert in the Action queue, so a newly broken sender or DNS change shows up before the grade drops.
An Ask AI button on each issue: Action queue items, failing or warning DNS checks, and senders with failing SPF/DKIM alignment. It opens a prompt with only the facts behind that issue (for example the domain, the current record, the sender's IPs and pass rates), shown in a preview first. From there it opens in Claude or ChatGPT with the prompt filled in, or copies it for Gemini and other AI tools. The link opens in your own browser; the app itself never contacts an AI service. Organization names, user emails, mailbox addresses and raw reports are never included. It's opt-in per organization: off until an org admin turns it on, either in the onboarding wizard or later in Settings.
A bell in the top bar with a red dot for notifications nobody has acted on yet, stored per organization. The worker creates them as reports come in, instead of the app scanning reports on every page load:
- New domain detected: opens Domains to add or dismiss it.
- New DKIM selector detected: opens that domain's DKIM selectors. If the sender signing with it isn't approved yet, it opens that sender first, so it gets approved or rejected before the selector is monitored.
A notification counts as acted on once its domain, selector or sender is handled, whoever did it. It replaces the "New in your reports" notice on the Overview.
Update now in the admin console for Portainer stacks: the stack's updater asks Portainer's API to redeploy the stack with the new version, so Portainer stays in charge of it. See Deploying with Portainer.
The 90 days behind each domain's grade, the failing-messages count, "ready to enforce", the Action queue's sender items and the Senders list's default range become a setting per organization: 30, 60, 90 or 180 days. Shorter reacts faster to a fix; longer is steadier.
The STARTTLS check connects to each MX host on port 25, which Azure (and many other cloud hosts) block. Instead, it can read the TLS-RPT reports senders like Google and Microsoft already send: how many connections to each MX host used TLS, and why any failed. That shows what real senders experienced, not one test connection.
A setting picks where the result comes from:
- Port 25 probe: what it does today; the default where port 25 is open.
- TLS-RPT reports: the default on Azure.
- Off: no STARTTLS check at all.
Per domain, with TLS-RPT reports as the source:
- Domain's TLS-RPT record points at the connected or hosted mailbox, and reports have come in: pass or fail from those reports.
- Record is set up, but no reports yet: shown as waiting for reports, neither passed nor failed, and left out of the grade until reports arrive.
- No TLS-RPT record reaching the app's mailbox, on Azure: the check is off for that domain and left out of the grade, with a hint that adding TLS-RPT enables it.
Today reports come in through a Microsoft 365 mailbox (Graph) or the server's hosted mailbox. v0.3.0 adds an IMAP connector (app password, or OAuth2 where the provider offers it) and Google Workspace mailboxes.
Where reports come from no longer depends on how people sign in: every organization picks its report source in Settings, whether it signs in with Microsoft, Google, another provider or local accounts:
- Microsoft 365 mailbox
- Google Workspace mailbox
- any IMAP mailbox (self-hosted Postfix/Dovecot, Fastmail, and so on)
- the server's hosted mailbox
An organization with local accounts and no hosted mailbox on the server can then still receive reports. DNS checks alone keep working without any mailbox.
Trusted reporters (in the v0.3.0 betas, Settings → General). Anyone can send a DMARC report to a
domain's rua= address, and today a sender with its own DMARC-protected
domain can pass the report sender check and add made-up data. Each
organization sets how strictly reports are checked: reports from known,
trusted reporters (Google, Microsoft, Yahoo and the like, plus any the
organization adds) count straight away, while reports from an unknown
reporter are shown but don't count towards ratings, the sender list or Ask
AI until an org admin accepts that reporter.
Microsoft sign-in already uses OpenID Connect (OIDC). v0.3.0 turns that into one sign-in path with three options per organization:
- Microsoft (as today)
- Google Workspace: "Sign in with Google", the organization linked to its Google Workspace domain
- Any OIDC provider: Authentik, Keycloak, Zitadel, Authelia, Okta, Auth0 and the like. The organization's admin enters the provider's issuer URL, client ID and secret; the rest comes from the provider's discovery document.
People sign in with their own account and 2FA is handled by their provider.
Finding the right provider from one login page. Microsoft and Google are one app for the whole server, so their buttons work for everyone. An organization's own provider isn't, so the login page asks first:
- "Sign in with your company account": the person enters their email address.
- YADT looks up the email domain. It routes only to organizations that have verified that domain in YADT, so nobody can claim someone else's domain, and public email domains such as gmail.com never route.
- The browser goes straight to that organization's provider, with the email address filled in.
- The provider returns to one shared callback address. YADT checks the signed token against the settings of the organization it sent the person to (not against what comes back): the right provider, and an email address on that domain.
Each organization also gets a direct link, for example
https://dmarc.example.com/login/acme, that skips the email step: handy to
share or bookmark. The login page never lists organizations. An unknown
domain gets "There's no company sign-in for this email address", plus the
other ways to sign in.
The expected admin, not the first sign-in. When creating an organization, the platform admin enters the email address of its admin. Only that person becomes org admin on their first sign-in, with Microsoft, Google or another provider; everyone else becomes a member. Today the first person in the tenant to sign in becomes admin.
The Google app is set in the env like the Microsoft apps, until the server settings page (v0.4.0).
Most optional settings move from the env file to a Server settings page
in the platform admin, with a first-run wizard over the same sections, so an
env file only needs the basics (database, FERNET_KEY, PUBLIC_BASE_URL,
updater secret). Sections:
- Microsoft 365 apps: the mail access app and the sign-in app.
- Google Workspace: the sign-in app and report ingestion. (Other OIDC providers are set per organization, in that organization's settings.)
- Report mailbox: the hosted reporting mailbox, and IMAP (see above). Without one, organizations that sign in without Microsoft can't receive reports, so the wizard makes this hard to miss.
- Cloudflare: for the hosted mailbox's authorization records.
- Instance details: security contact, MTA-STS policy, masking sign-in IPs, the update check.
A value set in the env still wins: the page shows it as "Set by the server's environment", read-only. Existing installs and Azure templates keep working unchanged, and a setting moves to the page by removing it from the env. Each section has a Test button that checks the values live.
Secrets are stored encrypted with FERNET_KEY, like connected mailbox tokens
and 2FA secrets today, and protected by the hardening below.
Done in v0.2.0: the app runs as a non-root user on a read-only filesystem, GitHub push protection for secrets, and docs for limiting the Microsoft mail app to the reports mailbox (RBAC for Applications) and for scoped Cloudflare tokens. Planned:
-
Server settings safeguards: secrets can be written but never read back
through the API; changing a setting needs your password and 2FA code
again;
SERVER_SETTINGS_LOCKED=truemakes the page read-only for installs that keep everything in the env (for example Azure with Key Vault); the settings table is closed to everything running for an organization; every change is logged and shows up as a notification for platform admins. -
FERNET_KEYrotation: two keys at once and a job that re-encrypts everything, so a leaked key can be replaced. - Passkeys for every local account: platform admins, org admins and users get phishing-resistant sign-in, with TOTP as fallback; managed on the Account tab, and cleared along with 2FA when an admin resets it. Optionally required: an org admin can require a passkey for the organization's local accounts, and a platform admin for the platform admins; anyone without one is asked to add it at their next sign-in. Accounts that sign in with Microsoft get this from Entra instead (passkeys and Conditional Access in the tenant).
- Signed images: release images signed with build provenance, checked by the updater before it updates.
- Azure: secrets wrapped by a Key Vault key through managed identity, so a stolen database and env aren't enough; a managed identity or federated credential instead of the Microsoft client secret where possible.
- Nothing for anonymous visitors to fingerprint: the API stops showing the version to anyone not signed in (the updater gets a token instead, which needs a change to the Azure template), and the frontend is split: a small, rarely changing public part with only the pages needed before signing in (login, 2FA, set password, admin login), and the app itself served only with a valid session. Today the login page loads the whole app, and its file name gives away the exact version.
- A separate database role for cross-organization work, so the role the app normally runs as can't switch row-level security off at all. Today it can, which a single SQL-injection bug could use to read every organization (none is known; queries use bound parameters).
- Our own resolver image on Alpine's maintained Unbound package, for Docker Compose and Azure alike. The current base image (mvance/unbound) hasn't been updated since October 2024.
- Wiki page: putting YADT behind an identity-aware proxy, so unknown visitors get nothing at all: Cloudflare Access, Entra Application Proxy or Front Door with Entra sign-in on Azure, or a VPN such as Tailscale. Reports come in through Microsoft Graph or IMAP, outbound, so nothing inbound needs an exception.
For managed service providers who run YADT for their customers. A switch in the platform admin turns MSP mode on; with it off, YADT works as it does today for a single organization, without any MSP screens.
The MSP's own organization. The MSP works from an operator organization: its own domains are monitored there like any customer's, and its staff are that organization's users. They sign in the same ways as any user: Microsoft, Google Workspace or another OIDC provider (v0.3.0), with passkeys from v0.4.0. Admins of the operator organization reach the platform admin with that same sign-in, so there's no separate set of admin accounts to manage. The platform's own admin account stays, for emergencies only.
MSP roles.
- MSP admin: MSP settings (including MSP mode itself), MSP users, and the per-organization DNS verification switch below.
- MSP engineer: opens customer organizations and works in them.
- MSP viewer: opens customer organizations read-only.
Opening a customer's dashboard. An MSP user opens a customer organization from the platform admin as a delegated session: no account is created in that organization, so an MSP user can never become a customer's first org admin (together with v0.3.0's "expected admin"). A banner shows whose dashboard it is and who is looking, for example "Viewing Customer X as Jan (MSP)".
Customer control. A customer's org admin decides how the MSP gets in: always, only after approving each visit, or not at all. The choice is in the customer's own settings and is logged.
Audit log, in two views.
- For the MSP: every action by MSP staff, across all organizations.
- For the customer: in their own settings, every MSP visit (who, when, from which IP address) and what was viewed or changed during it.
Both can be filtered by date, user, organization, action type and result, and exported as CSV for compliance.
Skipping DNS verification for an organization. MSP admins can switch domain verification off for one organization, for example while the MSP sets up a customer's DNS itself. It always has an end date and switches itself back on, shows as a badge on the organization, and is logged. Domains added meanwhile are marked "added without verification".
Today the API serves the dashboard itself: it signs in with a browser
session and two-factor authentication, so there's no supported way to
script against it. API tokens — created per organization in Settings,
read-only to start, revocable, and logged — would let you pull pass rates,
sender inventory and DNS check results into your own monitoring, reporting
or scripts. The interactive API docs (/docs, /openapi.json, off by
default since v0.1.5) would then be published as the reference for them.
A real message queue and multiple workers, for the many-tenant / very-high-volume case that the current Postgres-backed work queue deliberately doesn't target at the highest end. See Scaling Horizontally for what's already supported today.
Extend the database-backed test suite (which now covers row-level security) to the report-ingestion pipeline and HTTP routers. See Running the Test Suite for what's covered today.
← Back to the README · Live demo · Report an issue · MIT licensed
- Quick Start: Docker Compose
- Deploying with Portainer
- Deploying on Azure
- Configuration Reference
- Onboarding an Organization
- IMAP Mailbox
- Google Workspace Mailbox
- Sign-in Providers
- Multi-Tenancy & RLS
- DMARC Subdomain Inheritance
- Report Attribution
- SPF Conditional Mode
- Detected Domains
- DNS Check Scheduling
- Domain Rating
🏗️ Architecture
🗺️ Roadmap