No description
  • Rust 98.8%
  • Shell 1.2%
Find a file
2026-07-08 10:44:24 +02:00
scripts add package script 2026-07-08 10:44:24 +02:00
src initial commit 2026-07-07 23:20:32 +02:00
.gitignore initial commit 2026-07-07 23:20:32 +02:00
.rustfmt.toml initial commit 2026-07-07 23:20:32 +02:00
Cargo.lock add README.md and LICENSE 2026-07-08 10:26:55 +02:00
Cargo.toml add README.md and LICENSE 2026-07-08 10:26:55 +02:00
LICENSE add README.md and LICENSE 2026-07-08 10:26:55 +02:00
README.md add README.md and LICENSE 2026-07-08 10:26:55 +02:00

stward

A command-line tool for verifying and managing the DNS records of a Stalwart mail server.

Stalwart auto-generates the DNS records each domain needs (DKIM, SPF, DMARC, MX, SRV, MTA-STS, TLS-RPT, CAA, autoconfig/autodiscover, and optionally DANE TLSA) and can publish them through a configured DNS provider. stward closes the loop: it reads the server's intended zone via JMAP, queries the live authoritative nameservers, and tells you exactly which records are correct, missing, or mismatched. It can then republish missing records and manage which record categories a domain publishes.

The name is a nod to a steward: the administrator who runs an entire estate, not a single chore. DNS is where stward starts, not where it is required to stop.

Features

  • Verify every managed domain's records against what is actually live on the authoritative nameservers, in parallel, with retries.
  • Trace mode prints expected-vs-actual rdata per record set so mismatches are readable at a glance.
  • Publish-aware filtering only checks record categories a domain is actually configured to publish, eliminating false positives.
  • Renew triggers Stalwart's DNS reconcile task for selected record types and domains.
  • DNSSEC report checks each domain's parent zone for a DS record to determine whether the zone is signed.
  • Enable flips record categories on in a domain's publishRecords and reconciles them, with TLSA gated behind a DNSSEC check.
  • Correct handling of the awkward cases: multi-line parenthesised RSA DKIM keys, CAA separator whitespace, and order-independent TLSA/multi-record set matching.

Installation

cargo install --path .

Requires a recent stable Rust toolchain.

Authentication

Every command talks to the Stalwart JMAP API and needs a server URL and an API token with the relevant management permissions (sysDomainUpdate for enable):

stward --url https://mail.example.com --token "$STALWART_TOKEN" <command>

Commands

verify

Query the live authoritative nameservers and report the state of each managed domain's records.

stward --url ... --token ... verify
stward ... verify --domain example.com --trace --attempts 1
stward ... verify --types caa,tlsa
Flag Description
--nameserver <ADDR> Query this authoritative nameserver directly instead of resolving the domain's own NS.
--bootstrap <ADDR> Recursive resolver used to bootstrap NS discovery.
--attempts <N> Number of query attempts per record before giving up.
--retry-delay <SECS> Delay between attempts.
--query-delay <MS> Delay between individual record queries.
--types <LIST> Comma-separated record types to restrict the check to.
--timeout <SECS> Overall timeout for the run.
--trace Print expected vs actual rdata for every record set. Use with --attempts 1 for a clean single pass.
--domain <NAME> Restrict to these domains. Repeatable.

When records are missing, verify prints a ready-to-run renew command for the affected categories.

renew

Trigger Stalwart's DNS reconcile task to (re)publish records.

stward ... renew --records tlsa --domain example.com
stward ... renew --dry-run
Flag Description
--records <LIST> Comma-separated record categories to publish. Intersected with each domain's publishRecords.
--renew-certificate Also request certificate renewal on success.
--domain <NAME> Restrict to these domains. Repeatable.
--dry-run Show the intended task without submitting it.

dnssec

Report DNSSEC status per domain by checking for a DS record in the parent zone, and whether TLSA can be safely enabled.

stward ... dnssec
stward ... dnssec --domain example.com
Flag Description
--resolver <ADDR> Recursive resolver to query DS through. Defaults to 1.1.1.1:53.
--domain <NAME> Restrict to these domains. Repeatable.

Output verdicts:

Verdict Meaning
SAFE TO ENABLE Zone is signed, TLSA not yet published.
already on Zone is signed, TLSA already published.
PUBLISHED BUT UNSIGNED TLSA published into an unsigned zone. Remove it or sign the zone.
leave off (unsigned) Zone unsigned, TLSA correctly absent.

enable

Add record categories to a domain's publishRecords and reconcile them. Enabling tlsa is gated on the zone being DNSSEC-signed.

stward ... enable --records tlsa --dry-run
stward ... enable --records tlsa --domain example.com --domain other.com
stward ... enable --records caa,mtaSts --no-renew
Flag Description
--records <LIST> Record categories to enable (publishRecords keys, e.g. tlsa, caa, mtaSts). Required.
--resolver <ADDR> Recursive resolver for the DS check that gates tlsa. Defaults to 1.1.1.1:53.
--domain <NAME> Restrict to these domains. Repeatable.
--no-renew Update publishRecords only; skip the follow-up reconcile task.
--dry-run Print intended changes without writing anything.

enable runs in two phases: an x:Domain/set that flips the publishRecords keys, followed by an x:Task/set reconcile scoped to only the newly enabled records. --dry-run proves the selection with zero writes; always run it first.

dump

Print the raw JMAP domain objects and schema for inspection.

stward ... dump

How verification works

For each domain, stward resolves the authoritative nameservers (or uses one you specify), then queries each expected record set directly against them over TCP. Comparison is done per record set and is order-independent, so multiple TLSA anchors or MX records returned in a different order than expected still match.

A few record types get special handling because naive comparison produces false mismatches:

  • RSA DKIM keys are emitted as multi-line, parenthesised, multi-chunk TXT records. These are flattened before parsing to avoid the underlying zone parser treating a semicolon inside a quoted string as a comment.
  • CAA records normalise whitespace around ; and = separators before comparison, since that spacing is not significant per RFC 8659.
  • TXT records compare on concatenated character-string bytes, independent of how the authoritative server re-chunks them.

Notes on DANE / TLSA

TLSA records are only meaningful in a DNSSEC-signed zone, because DANE's trust model depends on the DNS answer being cryptographically authenticated. Publishing TLSA into an unsigned zone is dead weight; publishing anchors that do not match the live certificate in a signed zone can cause DANE-validating senders to refuse delivery. For this reason enable --records tlsa refuses to act on domains without a DS record, and dnssec exists to show you the signed vs unsigned split before you enable anything.

License

MIT