← For Dutch municipalities

Secure mail (NTA 7516)

Sending sensitive personal data to someone outside the organization, the way the Dutch secure-mail standard NTA 7516:2019 describes it. Built for the sociaal domein of a gemeente, which mails residents and partners about benefits, care and debts.

Built for NTA 7516, not certified against it. The norm text is sold by NEN and its successor NEN 7516 was not published at the time of writing. This page describes what the software does; conformity needs the NEN text and, ideally, an audit. The interoperability part (exchanging messages with other NTA 7516 platforms through their own channels) is not built; see Limits.

What happens on a secure send

The sender turns on Send securely in the composer (the shield button), or a DLP rule with the action Send securely matches (for example “BSN to external recipients”, compliance.md). Then each recipient gets the message one of three ways:

Recipient How Why it’s safe
A colleague (one of the org’s own domains) In their mailbox, as always Never leaves the server
On a trusted domain By mail Postfix may only deliver over TLS it has verified (MTA-STS, DANE or a valid certificate); otherwise the mail waits in the queue, never plaintext
Anyone else A notification with a link; they read it in the portal after a one-time code Content stays on the server; access needs the link and the code

The headers of the relayed copy still list every recipient; only the SMTP envelope leaves the portal recipients out, so they never get the content by mail. The notification names the sender and nothing else: no subject, no content.

The portal

/s/<token> in the web app, reachable without an account:

  1. The page names the sender and the organization and offers to send a code.
  2. The 6-digit code goes by SMS when the sender entered the recipient’s mobile number and an SMS gateway is configured; otherwise by e-mail. SMS is a real second factor (the link proves the mailbox, the code the phone); by e-mail both arrive in the same mailbox, which is weaker, and the admin page says which one is in use.
  3. The code is valid for 10 minutes and works once; a new one can be requested once a minute, ten in total. Five wrong codes lock the link for 15 minutes.
  4. The right code opens a 30-minute session (kept in the page’s memory only): the message, its attachments to download, and a reply with attachments. The reply lands in the sender’s inbox, threaded under the original and marked as external.

Link token, code and session token are stored as SHA-256 hashes. Every step is in the audit trail: securemail.send, .code, .open (success and failure), .download, .reply, .revoke, with the recipient’s address and IP.

For the sender

A Sent item that went out securely shows a panel: until when it is available, and per portal recipient whether they opened it (when, how often) and replied. Withdraw stops the portal at once, including open sessions; what someone already read, they keep.

Trusted domains

Admin → Mail security → Secure mail. Adding a domain checks it right away and again every day:

Result When Postfix policy
Trusted (MTA-STS) The domain publishes an MTA-STS policy in enforce mode that covers every MX secure match=<the policy's MX patterns>
Trusted (DANE) Every MX has TLSA records, and the mail stack validates DNSSEC (mail.secureMail.daneEnforced) dane-only
Trusted (certificate) Every MX offers STARTTLS with a certificate valid for its name (checked from the API over port 25) secure match=<the MX names>
Portal None of the above none: secure mail to this domain uses the portal

Postfix reads the policy through smtp_tls_policy_maps (deploy/mail/postfix/pgsql/tls-policy.cf, in the Helm ConfigMap too). The policy applies to all mail to that domain, not only secure mail: once a partner is trusted, nothing goes to it unencrypted. A domain that fails a later check loses its policy; its secure mail goes through the portal from then on.

DANE needs Postfix to validate DNSSEC (smtp_dns_support_level = dnssec) and a resolver that does it. The Helm value mail.secureMail.daneEnforced sets both sides; leave it off unless the cluster resolver answers with the AD bit, or mail to DANE domains would stay deferred.

Expiry

A secure message is available for 30 days by default (Admin → Secure mail, 1–365). After that its content and attachment references are removed; the row stays as the record of who opened it. The attachments themselves are the Sent copy’s: the blob janitor keeps them as long as a secure message serves them, even when the sender deleted the Sent copy (privacy.md).

Configuration

Variable Default Meaning
SECURE_MAIL_SMS_URL empty SMS gateway: a JSON POST {"to": "+31…", "text": "…"}. Empty = codes by e-mail. Helm config.secureMail.smsUrl.
SECURE_MAIL_SMS_TOKEN empty Bearer token for the gateway; put it in the app Secret.
SECURE_MAIL_CODE_TTL_MIN 10 How long a code is valid.
SECURE_MAIL_SESSION_MIN 30 How long an opened portal session lasts.
SECURE_MAIL_SWEEP_INTERVAL_MS 3600000 Expiry purge interval (the domain re-check runs daily); 0 = off.
SECURE_MAIL_DANE_ENFORCED false Set by Helm mail.secureMail.daneEnforced.

The SMS gateway is an outbound integration: it shows in the data-residency report and the register of processing, and DATA_RESIDENCY=eu-strict requires it on the EU allow-list. Dutch EU gateways (e.g. CM.com, MessageBird/Bird) take a small adapter for their own API shape.

API

  • POST /api/v1/mail/send with secure: true and optional securePhones: {"<address>": "<phone>"}; the reply gains secure: {id, portal[], direct[]}. Scheduled sends keep both fields.
  • Sender: GET /secure-mail/context (own and trusted domains, SMS on/off), GET /secure-mail/messages?sentMessageId=, PATCH /secure-mail/messages/{id} {revoked: true}.
  • Portal: GET /secure-mail/p/{token}, POST …/codes, POST …/sessions {code}, GET …/message, GET …/attachments/{index}, POST …/replies (session in the X-Secure-Session header).
  • Admin: GET /admin/secure-mail, PUT /admin/secure-mail/policy, POST|DELETE /admin/secure-mail/domains[/{id}], POST /admin/secure-mail/domains/{id}/checks.

Limits

  • Native SMTP clients. Mail sent from Outlook, Thunderbird or a phone over SMTP submission (587/465) is relayed by Postfix directly; the app’s DLP and secure route don’t see it. Mail through the web app, the desktop apps, JMAP, EWS, ActiveSync and MAPI goes through the app and is covered. Trusted-domain TLS enforcement (Postfix) does apply to all of it.
  • Drafts don’t keep the secure setting; turn it on again when you reopen one.
  • Interoperability. No exchange with other NTA 7516 platforms (ZorgMail, Zivver, …) through their own protocols. A recipient on such a platform gets the message over enforced TLS (if their domain is trusted) or through the portal.
  • Portal replies and native IMAP. A reply is filed in the sender’s mailbox in the app; whether a native IMAP client sees it depends on the IMAP mirror and has not been tested.
  • Availability (the norm asks 99.8% a year) is the operator’s: replicas, monitoring and backups (operations.md).

Regression

  • apps/api/test/smoke-securemail.mjs (57 checks, in run-all.mjs; runs a fake SMS gateway on :4199 and reads mailpit): the split, the envelope, notification contents, codes by SMS and e-mail, lockout, session, content, attachment, reply, sender status, withdraw, the SECURE DLP rule, a scheduled secure message, the attachment surviving the janitor, expiry setting, trusted domains against gmail.com (MTA-STS) and example.com (null MX; SMOKE_OFFLINE=1 skips those), audit entries.
  • apps/api/test/securemail-policy.test.ts: which Postfix policy a domain gets.
  • apps/web/e2e/secure-portal.spec.ts (mocked, axe at 1280/390) and secure-compose.spec.ts (live: notice, toggle, Sent panel, withdraw).