- Rust 98.8%
- Shell 1.2%
| scripts | ||
| src | ||
| .gitignore | ||
| .rustfmt.toml | ||
| Cargo.lock | ||
| Cargo.toml | ||
| LICENSE | ||
| README.md | ||
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
publishRecordsand 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