v1.0.2
No files matched your search
@@ -33,6 +33,7 @@ export default defineConfig({
|
||||
redirects: {
|
||||
"/docs": "/docs/install/",
|
||||
"/docs/install/get-started": "/docs/install/",
|
||||
"/docs/install/setup-wizard": "/docs/install/platform/",
|
||||
"/docs/management/tasks": "/docs/management/tasks-actions/tasks",
|
||||
"/docs/management/actions": "/docs/management/tasks-actions/actions",
|
||||
"/docs/applications": "/docs/management/applications/",
|
||||
|
||||
@@ -12,11 +12,13 @@
|
||||
# now point at the renamed install landing page (Welcome to Stalwart).
|
||||
/docs /docs/install/ 301
|
||||
/docs/install/get-started /docs/install/ 301
|
||||
/docs/install/setup-wizard /docs/install/platform/ 301
|
||||
|
||||
# Section overview pages were renamed to index.md; redirect old URLs.
|
||||
/docs/storage/overview /docs/storage/ 301
|
||||
/docs/auth/overview /docs/auth/ 301
|
||||
/docs/encryption/overview /docs/encryption/ 301
|
||||
/docs/encryption/manage /docs/encryption/at-rest 301
|
||||
/docs/mta/overview /docs/mta/ 301
|
||||
/docs/email/overview /docs/email/ 301
|
||||
/docs/collaboration/overview /docs/collaboration/ 301
|
||||
|
||||
|
Before Width: | Height: | Size: 292 KiB |
|
After Width: | Height: | Size: 61 KiB |
|
After Width: | Height: | Size: 63 KiB |
|
Before Width: | Height: | Size: 220 KiB |
|
After Width: | Height: | Size: 50 KiB |
|
After Width: | Height: | Size: 52 KiB |
|
After Width: | Height: | Size: 68 KiB |
|
After Width: | Height: | Size: 72 KiB |
|
Before Width: | Height: | Size: 309 KiB |
|
After Width: | Height: | Size: 52 KiB |
|
After Width: | Height: | Size: 56 KiB |
|
Before Width: | Height: | Size: 545 KiB |
|
After Width: | Height: | Size: 69 KiB |
|
After Width: | Height: | Size: 74 KiB |
|
Before Width: | Height: | Size: 815 KiB |
|
After Width: | Height: | Size: 61 KiB |
|
After Width: | Height: | Size: 65 KiB |
|
Before Width: | Height: | Size: 819 KiB |
|
After Width: | Height: | Size: 57 KiB |
|
After Width: | Height: | Size: 61 KiB |
|
Before Width: | Height: | Size: 620 KiB |
|
After Width: | Height: | Size: 71 KiB |
|
After Width: | Height: | Size: 75 KiB |
|
Before Width: | Height: | Size: 623 KiB |
|
Before Width: | Height: | Size: 731 KiB |
|
After Width: | Height: | Size: 61 KiB |
|
Before Width: | Height: | Size: 301 KiB |
|
After Width: | Height: | Size: 66 KiB |
|
Before Width: | Height: | Size: 686 KiB |
|
After Width: | Height: | Size: 50 KiB |
|
After Width: | Height: | Size: 54 KiB |
|
Before Width: | Height: | Size: 773 KiB |
|
After Width: | Height: | Size: 47 KiB |
|
After Width: | Height: | Size: 50 KiB |
|
After Width: | Height: | Size: 45 KiB |
|
After Width: | Height: | Size: 46 KiB |
|
Before Width: | Height: | Size: 564 KiB |
|
Before Width: | Height: | Size: 352 KiB |
|
After Width: | Height: | Size: 55 KiB |
|
After Width: | Height: | Size: 58 KiB |
|
After Width: | Height: | Size: 49 KiB |
|
After Width: | Height: | Size: 51 KiB |
|
Before Width: | Height: | Size: 655 KiB |
|
Before Width: | Height: | Size: 365 KiB |
|
After Width: | Height: | Size: 35 KiB |
|
After Width: | Height: | Size: 36 KiB |
|
Before Width: | Height: | Size: 557 KiB |
|
Before Width: | Height: | Size: 285 KiB |
|
After Width: | Height: | Size: 29 KiB |
|
After Width: | Height: | Size: 53 KiB |
|
Before Width: | Height: | Size: 658 KiB |
|
Before Width: | Height: | Size: 759 KiB |
|
After Width: | Height: | Size: 46 KiB |
|
After Width: | Height: | Size: 47 KiB |
|
Before Width: | Height: | Size: 456 KiB |
|
After Width: | Height: | Size: 60 KiB |
|
After Width: | Height: | Size: 62 KiB |
|
Before Width: | Height: | Size: 331 KiB |
|
Before Width: | Height: | Size: 660 KiB |
|
After Width: | Height: | Size: 36 KiB |
|
After Width: | Height: | Size: 38 KiB |
|
Before Width: | Height: | Size: 733 KiB |
|
Before Width: | Height: | Size: 291 KiB |
|
Before Width: | Height: | Size: 527 KiB |
|
Before Width: | Height: | Size: 181 KiB |
|
Before Width: | Height: | Size: 201 KiB |
|
After Width: | Height: | Size: 35 KiB |
|
After Width: | Height: | Size: 37 KiB |
|
After Width: | Height: | Size: 60 KiB |
|
After Width: | Height: | Size: 63 KiB |
|
After Width: | Height: | Size: 26 KiB |
|
After Width: | Height: | Size: 26 KiB |
|
After Width: | Height: | Size: 27 KiB |
|
After Width: | Height: | Size: 29 KiB |
|
After Width: | Height: | Size: 36 KiB |
|
After Width: | Height: | Size: 38 KiB |
|
After Width: | Height: | Size: 32 KiB |
|
After Width: | Height: | Size: 34 KiB |
@@ -21,7 +21,9 @@ rewrite is idempotent: running the script again after a schema change
|
||||
updates every consumer page in one go.
|
||||
|
||||
`Name` may also be `Object/View` (e.g. `MemoryLookupKey/SpamTrap`) to insert
|
||||
only the path of that view.
|
||||
only the path of that view, or `Object/` (e.g. `ExternalPublicKey/`) to insert
|
||||
only the object's own list when it also has views. A bare `Object` inserts
|
||||
every location.
|
||||
|
||||
When the consumer file is `.mdx` the script also rewrites the SVG attributes
|
||||
inside the breadcrumb HTML to camelCase (`stroke-width` -> `strokeWidth`)
|
||||
@@ -142,6 +144,8 @@ def load_breadcrumbs() -> dict[str, str]:
|
||||
for target, html in spans:
|
||||
if "/" in target:
|
||||
mapping[target] = html.strip()
|
||||
elif target == name:
|
||||
mapping[f"{name}/"] = html.strip()
|
||||
else:
|
||||
parts = [p.strip() for p in INTER_BREADCRUMB_RE.split(raw) if p.strip()]
|
||||
mapping[name] = BREADCRUMB_SEPARATOR.join(parts)
|
||||
|
||||
@@ -0,0 +1,136 @@
|
||||
---
|
||||
import Icon from "./Icon.astro";
|
||||
|
||||
const families = [
|
||||
{
|
||||
name: "OpenPGP",
|
||||
sub: "v4 and v6 keys, RFC 9580",
|
||||
groups: [
|
||||
{ label: "Key types", chips: ["RSA", "ECDH P-256 · P-384 · P-521", "Curve25519", "X25519", "X448"] },
|
||||
{ label: "Encryption", chips: ["AES-OCB", "AES-GCM", "AES-CFB"] },
|
||||
],
|
||||
},
|
||||
{
|
||||
name: "S/MIME",
|
||||
sub: "X.509 certificates, CMS",
|
||||
groups: [
|
||||
{ label: "Key types", chips: ["RSA", "RSA-OAEP", "ECDH P-256 · P-384 · P-521", "X25519", "X448"] },
|
||||
{ label: "Encryption", chips: ["AES-256-GCM", "AES-256-CBC"] },
|
||||
],
|
||||
},
|
||||
];
|
||||
---
|
||||
<div class="dg" role="img" aria-label="Encryption algorithms supported by Stalwart. OpenPGP v4 and v6 keys with RSA, ECDH on NIST P-256, P-384 and P-521, Curve25519, X25519 and X448, encrypting with AES-OCB, AES-GCM or AES-CFB. S/MIME certificates with RSA, RSA-OAEP, ECDH on P-256, P-384 and P-521, X25519 and X448, encrypting with AES-256-GCM or AES-256-CBC. Post-quantum ML-KEM for both: ML-KEM-768 with X25519 and ML-KEM-1024 with X448 for OpenPGP, and ML-KEM-512, ML-KEM-768 and ML-KEM-1024 certificates for S/MIME.">
|
||||
<span class="dg-caption">Whatever key the user already has</span>
|
||||
<div class="cols">
|
||||
{families.map((f) => (
|
||||
<div class="fam">
|
||||
<strong>{f.name}</strong>
|
||||
<span class="sub">{f.sub}</span>
|
||||
{f.groups.map((g) => (
|
||||
<div class="grp">
|
||||
<span class="lbl">{g.label}</span>
|
||||
<ul class="mini-chips">{g.chips.map((c) => <li>{c}</li>)}</ul>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
<div class="pq">
|
||||
<span class="ic"><Icon name="atom" size={20} /></span>
|
||||
<div class="body">
|
||||
<strong>Post-quantum, on both</strong>
|
||||
<span>ML-KEM (FIPS 203), the NIST standard for key exchange that resists quantum computers</span>
|
||||
<ul class="mini-chips">
|
||||
<li>ML-KEM-768 + X25519</li>
|
||||
<li>ML-KEM-1024 + X448</li>
|
||||
<li>S/MIME ML-KEM-512 · 768 · 1024</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.dg {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 14px;
|
||||
width: 100%;
|
||||
container-type: inline-size;
|
||||
}
|
||||
.cols {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
gap: 14px;
|
||||
}
|
||||
.fam {
|
||||
display: grid;
|
||||
align-content: start;
|
||||
gap: 6px;
|
||||
padding: 20px;
|
||||
border-radius: var(--r-lg);
|
||||
background: var(--tile);
|
||||
box-shadow: var(--shadow-tile);
|
||||
}
|
||||
strong {
|
||||
font-family: var(--font-display);
|
||||
font-size: 1rem;
|
||||
font-weight: 680;
|
||||
line-height: 1.3;
|
||||
}
|
||||
.sub,
|
||||
.body > span {
|
||||
color: var(--muted);
|
||||
font-size: var(--fs-xs);
|
||||
line-height: 1.5;
|
||||
}
|
||||
.grp {
|
||||
display: grid;
|
||||
gap: 4px;
|
||||
margin-top: 8px;
|
||||
}
|
||||
.lbl {
|
||||
color: var(--dim);
|
||||
font-size: 0.6875rem;
|
||||
font-weight: 600;
|
||||
}
|
||||
.pq {
|
||||
display: flex;
|
||||
gap: 16px;
|
||||
align-items: flex-start;
|
||||
padding: 20px;
|
||||
border-radius: var(--r-lg);
|
||||
background: var(--rose);
|
||||
}
|
||||
.pq .ic {
|
||||
display: grid;
|
||||
place-items: center;
|
||||
flex: none;
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
border-radius: 12px;
|
||||
background: var(--brand);
|
||||
color: #fff;
|
||||
}
|
||||
.body {
|
||||
display: grid;
|
||||
gap: 6px;
|
||||
min-width: 0;
|
||||
}
|
||||
.pq .mini-chips {
|
||||
margin-top: 6px;
|
||||
}
|
||||
.pq .mini-chips li {
|
||||
background: var(--tile);
|
||||
color: var(--brand-text);
|
||||
}
|
||||
@container (max-width: 520px) {
|
||||
.cols {
|
||||
grid-template-columns: minmax(0, 1fr);
|
||||
}
|
||||
.fam,
|
||||
.pq {
|
||||
padding: 16px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,182 @@
|
||||
---
|
||||
import Icon from "./Icon.astro";
|
||||
|
||||
const sources = ["Web Key Directory", "keys.openpgp.org", "HKP", "OPENPGPKEY", "SMIMEA", "Harvested"];
|
||||
|
||||
const copies = [
|
||||
{ to: "bob@partner.org", sub: "OpenPGP · ML-KEM-768 + X25519", icon: "lock-keyhole", brand: true },
|
||||
{ to: "carol@other.net", sub: "S/MIME · AES-256-GCM", icon: "lock-keyhole" },
|
||||
{ to: "dave@nokey.com", sub: "No key: sent as is, or bounced when required", icon: "mail-warning", muted: true },
|
||||
];
|
||||
---
|
||||
<div class="dg" role="img" aria-label="Encryption gateway: a plain message from alice@example.com to three recipients enters Stalwart, which looks up each recipient's key in Web Key Directory, keys.openpgp.org, HKP keyservers, DNSSEC-signed OPENPGPKEY and SMIMEA records or keys harvested from earlier mail, then sends one encrypted copy per recipient: OpenPGP with ML-KEM-768 and X25519 to bob@partner.org, S/MIME with AES-256-GCM to carol@other.net, and for dave@nokey.com, which has no key, the message is sent unencrypted or bounced when encryption is required.">
|
||||
<span class="dg-caption">One message in, one encrypted copy per recipient</span>
|
||||
<div class="flow">
|
||||
<div class="msg">
|
||||
<span class="hd"><Icon name="mail" size={15} />Plain message</span>
|
||||
<span class="row"><b>From</b> alice@example.com</span>
|
||||
<span class="row"><b>To</b> bob, carol, dave</span>
|
||||
<span class="row"><b>Subject</b> Q3 contract</span>
|
||||
</div>
|
||||
<span class="arrow"><Icon name="arrow-right" size={14} /></span>
|
||||
<div class="gw">
|
||||
<span class="hd"><Icon name="key-round" size={15} />Stalwart gateway</span>
|
||||
<span class="sub">Finds each recipient's public key</span>
|
||||
<ul class="mini-chips">{sources.map((s) => <li>{s}</li>)}</ul>
|
||||
</div>
|
||||
<span class="arrow"><Icon name="arrow-right" size={14} /></span>
|
||||
<ul class="copies">
|
||||
{copies.map((c) => (
|
||||
<li class:list={{ "is-brand": c.brand, "is-muted": c.muted }}>
|
||||
<span class="ic"><Icon name={c.icon} size={15} /></span>
|
||||
<div class="target">
|
||||
<code>{c.to}</code>
|
||||
<span>{c.sub}</span>
|
||||
</div>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.dg {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 14px;
|
||||
width: 100%;
|
||||
container-type: inline-size;
|
||||
}
|
||||
.flow {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(0, 0.9fr) auto minmax(0, 1fr) auto minmax(0, 1.3fr);
|
||||
gap: 10px;
|
||||
align-items: center;
|
||||
}
|
||||
.msg,
|
||||
.gw {
|
||||
display: grid;
|
||||
gap: 6px;
|
||||
padding: 16px;
|
||||
border-radius: var(--r-lg);
|
||||
background: var(--tile);
|
||||
box-shadow: var(--shadow-tile);
|
||||
}
|
||||
.gw {
|
||||
background: var(--rose);
|
||||
box-shadow: none;
|
||||
}
|
||||
.hd {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
margin-bottom: 4px;
|
||||
font-family: var(--font-display);
|
||||
font-size: 0.875rem;
|
||||
font-weight: 680;
|
||||
}
|
||||
.hd :global(svg) {
|
||||
flex: none;
|
||||
color: var(--brand-text);
|
||||
}
|
||||
.row {
|
||||
color: var(--muted);
|
||||
font-family: var(--font-mono);
|
||||
font-size: 0.6875rem;
|
||||
overflow-wrap: anywhere;
|
||||
}
|
||||
.row b {
|
||||
color: var(--text);
|
||||
font-weight: 600;
|
||||
}
|
||||
.sub {
|
||||
color: var(--muted);
|
||||
font-size: var(--fs-xs);
|
||||
line-height: 1.4;
|
||||
}
|
||||
.gw .mini-chips li {
|
||||
background: var(--tile);
|
||||
color: var(--brand-text);
|
||||
}
|
||||
.arrow {
|
||||
display: grid;
|
||||
place-items: center;
|
||||
width: 26px;
|
||||
height: 26px;
|
||||
border-radius: 50%;
|
||||
background: var(--brand);
|
||||
color: #fff;
|
||||
}
|
||||
.copies {
|
||||
display: grid;
|
||||
gap: 8px;
|
||||
padding: 0;
|
||||
}
|
||||
.copies li {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
.ic {
|
||||
display: grid;
|
||||
place-items: center;
|
||||
flex: none;
|
||||
width: 30px;
|
||||
height: 30px;
|
||||
border-radius: 10px;
|
||||
background: var(--sand);
|
||||
color: var(--text);
|
||||
}
|
||||
.target {
|
||||
display: grid;
|
||||
gap: 2px;
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
padding: 10px 12px;
|
||||
border-radius: var(--r-lg);
|
||||
background: var(--tile);
|
||||
box-shadow: var(--shadow-tile);
|
||||
}
|
||||
.target code {
|
||||
padding: 0;
|
||||
background: none;
|
||||
color: var(--text);
|
||||
font-family: var(--font-mono);
|
||||
font-size: 0.75rem;
|
||||
font-weight: 600;
|
||||
overflow-wrap: anywhere;
|
||||
}
|
||||
.target span {
|
||||
color: var(--muted);
|
||||
font-size: 0.6875rem;
|
||||
line-height: 1.4;
|
||||
}
|
||||
.is-brand .ic {
|
||||
background: var(--brand);
|
||||
color: #fff;
|
||||
}
|
||||
.is-brand .target {
|
||||
background: var(--rose);
|
||||
box-shadow: none;
|
||||
}
|
||||
.is-brand .target code {
|
||||
color: var(--brand-text);
|
||||
}
|
||||
.is-muted .target {
|
||||
background: none;
|
||||
box-shadow: inset 0 0 0 1px var(--hairline);
|
||||
}
|
||||
.is-muted .ic {
|
||||
color: var(--dim);
|
||||
}
|
||||
@container (max-width: 640px) {
|
||||
.flow {
|
||||
grid-template-columns: minmax(0, 1fr);
|
||||
justify-items: stretch;
|
||||
}
|
||||
.arrow {
|
||||
justify-self: center;
|
||||
transform: rotate(90deg);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -7,6 +7,7 @@ interface Props {
|
||||
ctaSub?: string;
|
||||
ctaButton?: string;
|
||||
ctaLink?: string;
|
||||
ctaSecondary?: { label: string; href: string; external?: boolean };
|
||||
hideCta?: boolean;
|
||||
}
|
||||
|
||||
@@ -15,6 +16,7 @@ const {
|
||||
ctaSub = "Install Stalwart in minutes, then keep it running for years.",
|
||||
ctaButton = "Get Stalwart",
|
||||
ctaLink = "/docs/install/",
|
||||
ctaSecondary = { label: "Documentation", href: "/docs/" },
|
||||
hideCta = false,
|
||||
} = Astro.props;
|
||||
|
||||
@@ -60,7 +62,7 @@ const socials = [
|
||||
<div class="cta-copy">
|
||||
<h2 id="footer-cta-title" class="h-cta">{cta}</h2>
|
||||
<p class="cta-sub">{ctaSub}</p>
|
||||
<div class="actions"><a class="btn btn-on-ink" href="/pricing">See pricing</a></div>
|
||||
<div class="actions"><a class="btn btn-on-ink" href={ctaSecondary.href} {...(ctaSecondary.external ? { target: "_blank", rel: "noopener" } : {})}>{ctaSecondary.label}</a></div>
|
||||
</div>
|
||||
<a class="cta-go" href={ctaLink} {...(ctaLink.startsWith("http") ? { target: "_blank", rel: "noopener" } : {})}>
|
||||
<span class="cta-go-ic"><Icon name="arrow-up-right" size={24} /></span>
|
||||
|
||||
@@ -89,9 +89,6 @@ const side = hero.visual ? "visual" : hero.aside ? "aside" : null;
|
||||
grid-column: span 6;
|
||||
min-height: 460px;
|
||||
}
|
||||
.hero-grid :global(.hero-visual.shot) {
|
||||
--vis: 0.62;
|
||||
}
|
||||
|
||||
.hero-banner {
|
||||
display: inline-flex;
|
||||
@@ -189,7 +186,6 @@ const side = hero.visual ? "visual" : hero.aside ? "aside" : null;
|
||||
|
||||
.hero-grid--stacked {
|
||||
min-height: 0;
|
||||
grid-auto-rows: 1fr;
|
||||
}
|
||||
.hero-grid--stacked .hero-copy {
|
||||
grid-column: 1 / -1;
|
||||
@@ -229,10 +225,17 @@ const side = hero.visual ? "visual" : hero.aside ? "aside" : null;
|
||||
grid-column: 2;
|
||||
grid-row: 4;
|
||||
}
|
||||
.hero-grid--stacked :global(.hero-visual) {
|
||||
.hero-grid.hero-grid--stacked :global(.hero-visual) {
|
||||
grid-column: 1 / -1;
|
||||
min-height: 0;
|
||||
}
|
||||
.hero-grid--stacked :global(.hero-visual.shot) {
|
||||
aspect-ratio: 16 / 9;
|
||||
}
|
||||
.hero-grid--stacked :global(.hero-visual.visual-dg svg) {
|
||||
max-width: 860px;
|
||||
margin-inline: auto;
|
||||
}
|
||||
|
||||
@media (max-width: 1023px) {
|
||||
.hero-grid {
|
||||
@@ -255,18 +258,8 @@ const side = hero.visual ? "visual" : hero.aside ? "aside" : null;
|
||||
.hero-grid :global(.hero-visual) {
|
||||
min-height: 340px;
|
||||
}
|
||||
.hero-grid :global(.hero-visual.shot) {
|
||||
--vis: 0.34;
|
||||
}
|
||||
.hero-grid--stacked {
|
||||
grid-auto-rows: auto;
|
||||
}
|
||||
.hero-grid--stacked :global(.hero-visual) {
|
||||
min-height: 0;
|
||||
.hero-grid--stacked :global(.hero-visual.shot) {
|
||||
aspect-ratio: 4 / 3;
|
||||
}
|
||||
.hero-grid--stacked :global(.hero-visual .shot-frame img) {
|
||||
width: calc(100cqw / 0.5);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -19,11 +19,17 @@ const layers = [
|
||||
icon: "lock-keyhole",
|
||||
title: "At rest, in the mailbox",
|
||||
sub: "Per-mailbox encryption with the user's own S/MIME cert or OpenPGP key",
|
||||
chips: ["S/MIME", "OpenPGP", "disk-access proof"],
|
||||
chips: ["S/MIME", "OpenPGP", "ML-KEM", "disk-access proof"],
|
||||
},
|
||||
{
|
||||
icon: "send",
|
||||
title: "On the way out, to each recipient",
|
||||
sub: "Encryption gateway with key discovery over WKD, keyservers and DNSSEC",
|
||||
chips: ["OpenPGP", "S/MIME", "header protection"],
|
||||
},
|
||||
];
|
||||
---
|
||||
<div class="dg" role="img" aria-label="Stalwart security stack: certificate management with ACME, encryption in transit between servers with DANE, MTA-STS, TLS-RPT and STARTTLS, and encryption at rest in the mailbox with S/MIME or OpenPGP">
|
||||
<div class="dg" role="img" aria-label="Stalwart security stack: certificate management with ACME, encryption in transit between servers with DANE, MTA-STS, TLS-RPT and STARTTLS, encryption at rest in the mailbox with S/MIME or OpenPGP including post-quantum ML-KEM, and an encryption gateway that encrypts outgoing mail for each recipient">
|
||||
<span class="dg-caption">Security from the wire to the mailbox</span>
|
||||
{layers.map((l, i) => (
|
||||
<div class:list={["layer", { "is-brand": l.brand }]} style={`--i: ${i}`}>
|
||||
|
||||
@@ -19,13 +19,16 @@ const placeholderCaption =
|
||||
: "";
|
||||
---
|
||||
{visual.kind === "screenshot" && (
|
||||
<figure class:list={["tile tile--ink shot", { "shot--full": full }, cls]}>
|
||||
<figure class:list={["tile shot", { "shot--full": full }, cls]} style={`--cw: ${visual.width}px; --ar: ${visual.width / visual.height};`}>
|
||||
<figcaption class="shot-url">
|
||||
<Icon name="lock" size={13} />
|
||||
<span>{visual.url ?? "admin.stalw.art"}</span>
|
||||
</figcaption>
|
||||
<div class="shot-frame">
|
||||
<img src={visual.src} alt={visual.alt} width="1280" height="800" loading={loading} decoding="async" />
|
||||
<img class:list={["shot-img", { "shot-img--light": visual.srcDark }]} src={visual.src} alt={visual.alt} width={visual.width} height={visual.height} loading={loading} decoding="async" />
|
||||
{visual.srcDark && (
|
||||
<img class="shot-img shot-img--dark" src={visual.srcDark} alt={visual.alt} width={visual.width} height={visual.height} loading={loading} decoding="async" />
|
||||
)}
|
||||
</div>
|
||||
</figure>
|
||||
)}
|
||||
|
||||
@@ -17,11 +17,11 @@ const visualSchema = z.discriminatedUnion("kind", [
|
||||
z.object({
|
||||
kind: z.literal("screenshot"),
|
||||
src: z.string(),
|
||||
srcDark: z.string().optional(),
|
||||
width: z.number().default(1180),
|
||||
height: z.number().default(844),
|
||||
alt: z.string(),
|
||||
url: z.string().optional(),
|
||||
glow: z.boolean().default(false),
|
||||
tilt: z.boolean().default(false),
|
||||
frame: z.boolean().default(true),
|
||||
}),
|
||||
z.object({
|
||||
kind: z.literal("diagram"),
|
||||
@@ -70,6 +70,7 @@ const ctaCardSchema = z.object({
|
||||
ctaSub: z.string(),
|
||||
ctaButton: z.string(),
|
||||
ctaLink: z.string(),
|
||||
secondary: ctaSchema.optional(),
|
||||
});
|
||||
|
||||
// ----- Section kinds -----
|
||||
|
||||
|
After Width: | Height: | Size: 304 KiB |
|
After Width: | Height: | Size: 313 KiB |
@@ -0,0 +1,114 @@
|
||||
---
|
||||
title: "Encryption Gateway and Post-Quantum Email Encryption"
|
||||
description: "Stalwart v1.0.2 brings an outbound encryption gateway, a new OpenPGP and S/MIME engine with modern and post-quantum algorithms, recipient keys kept up to date from published directories, and a restyled WebUI. All of it open source."
|
||||
authors: [mdecimus]
|
||||
tags: [release, encryption, openpgp, smime, post-quantum, webui]
|
||||
excerpt: |
|
||||
The first of the features held back from the v1.0 cut is here. Stalwart v1.0.2 introduces an encryption gateway that encrypts outgoing mail for its recipients, a new OpenPGP and S/MIME engine with modern and post-quantum algorithms, and a restyled WebUI. Everything in this post is open source.
|
||||
date: 2026-11-02
|
||||
---
|
||||
|
||||
import { Image } from "astro:assets";
|
||||
import dashboardLight from "./img/dashboard-overview-light.png";
|
||||
import dashboardDark from "./img/dashboard-overview-dark.png";
|
||||
|
||||
When we released [Stalwart v1.0](/blog/stalwart-1-0) last week, it carried only part of the work we had been doing. Several large features were being built in parallel, and for a while the plan was to ship them all together. In the end we chose a different path: v1.0 would be about performance and a stable database schema, and nothing else would be allowed to distract from that. The other features would follow over the next few weeks, one release at a time, each given the attention it deserves.
|
||||
|
||||
The first of them is encryption. Stalwart v1.0.2 introduces an **encryption gateway** that encrypts outgoing mail with each recipient's own public key, a completely new **OpenPGP and S/MIME engine** with modern algorithms, including **post-quantum encryption**, and a set of tools to find, verify and keep track of the keys of the people your users write to. And, as with everything in Stalwart's core, all of it is open source under the AGPL.
|
||||
|
||||
## Locks and keys
|
||||
|
||||
Email is well protected while it travels: TLS encrypts each connection between servers, so nobody on the network can read a message in transit. The protection ends when the message arrives. Every server along the way stores the message in a form that anyone with access to it can read, from the administrators to an attacker who copies a backup.
|
||||
|
||||
Message encryption closes that gap by protecting the message itself rather than the connection. It relies on public-key cryptography, which is easiest to picture as a padlock and its key. Your **public key** is like an open padlock: you can hand out copies to anyone, and anyone can use one to lock a box. Your **private key** is the only key that opens those padlocks, and it never leaves your hands. A message locked with your public key can be read by you and nobody else, which is why publishing the public key is safe.
|
||||
|
||||
Two standards apply this idea to email. [OpenPGP](/docs/encryption/pgp) lets people create their own key pairs with tools such as GnuPG or Thunderbird and publish the public half wherever they like. [S/MIME](/docs/encryption/smime) uses certificates issued by a certificate authority, in the same X.509 format as the certificates that secure websites but issued for email addresses, and it is built into Outlook, Apple Mail and most corporate environments. Stalwart supports both.
|
||||
|
||||
One principle runs through everything below: **Stalwart only ever holds public keys**. It encrypts, but it never decrypts and never stores a private key. A message it has encrypted cannot be read by Stalwart itself, by its administrators, or by anyone who later gets hold of its disks.
|
||||
|
||||
## Encryption at rest, rebuilt
|
||||
|
||||
[Encryption at rest](/docs/encryption/at-rest) has been part of Stalwart since 2023. Each user who wants it registers a public key, and from then on the server encrypts every message it delivers to that user before writing it to storage. Only the user's mail client, holding the private key, can open what is stored. For v1.0.2 we rebuilt it on top of the new engine described below, and it gained a good deal along the way.
|
||||
|
||||
There is no longer a cipher to choose. The server reads the key itself and picks the strongest format that the key's owner can actually read, following the preferences that the OpenPGP key announces or the type of the S/MIME certificate. A registered key entry can hold several keys or certificates, so a user who reads mail on devices with different keys, or who is rotating to a new key, can decrypt with any of them (when only some of them support post-quantum encryption, every message uses the format all of them can read). When a key is registered, the server describes it in plain terms (for example `OpenPGP v6: ML-KEM-1024+X448`), shows its fingerprints, and raises warnings before trouble arrives: a key that expires within two weeks, an RSA key shorter than 3072 bits, a key that names none of the account's addresses, or one that few mail clients could decrypt.
|
||||
|
||||
Stored messages can also carry [header protection](/docs/encryption/at-rest#header-protection) ([RFC 9788](https://www.rfc-editor.org/rfc/rfc9788)), a recent standard that places a copy of the sender, recipients and subject inside the encrypted part, where they cannot be altered without the client noticing; on stored mail the outer copies, subject included, stay readable. Two compatibility switches cover the quirks of real-world clients: one enables authenticated encryption for S/MIME users whose clients support it, and the other picks between the encoding Outlook expects and the one the standard prescribes for elliptic-curve certificates.
|
||||
|
||||
## Encryption gateway
|
||||
|
||||
Most people never set up OpenPGP or S/MIME, and most of those who do forget to use it. Organisations that must not send readable mail to certain partners have long relied on a dedicated appliance to fix this: an encryption gateway. Stalwart now has one built in.
|
||||
|
||||
The [encryption gateway](/docs/encryption/gateway) works on mail leaving the server, once an administrator turns it on. When a message is addressed to someone whose public key Stalwart knows, the gateway encrypts it with that key on the way out. The sender does nothing different and needs no software at all, and the recipient opens the message in their own client, with their own private key, exactly as if the sender had encrypted it by hand. Unlike TLS, the protection does not end at the next server: the message stays encrypted at the recipient's provider, in their mailbox and in every backup made along the way.
|
||||
|
||||
A policy decides what happens to each recipient. In **opportunistic** mode, every recipient with a known key receives encrypted mail and the rest receive it as usual. In **required** mode, a recipient without a usable key receives nothing in plain text: the sender gets a bounce instead. The policy is an [expression](/docs/configuration/expressions), so it can require encryption for one partner domain, stay opportunistic everywhere else and leave internal mail alone, all in a few lines.
|
||||
|
||||
Each encrypted recipient receives a copy of their own, encrypted for their key alone, A single shared copy would list the key of every recipient it was encrypted for; separate copies reveal nothing about who else received the message, which keeps blind-copy recipients blind. Every copy is signed again with DKIM and DKIM2, so it passes authentication checks at the other end just as the original would. And by default the gateway uses header protection to its full extent: the subject line, which often says as much as the message itself, is replaced with `[...]` on the outside and travels only inside the encryption.
|
||||
|
||||
## Finding the right key
|
||||
|
||||
A gateway is only as good as the keys it knows, and a wrong key is worse than none, because it hands the message to whoever holds the matching private key. Stalwart therefore treats recipient keys with care. They are [external keys](/docs/encryption/external-keys), managed by the system administrator and shared by every sender on the server, and they arrive in three ways.
|
||||
|
||||
The administrator can enter a key directly, when it came through a trusted channel. The administrator can also ask the server to follow a key that the recipient's organisation publishes, and Stalwart supports every common way of publishing one: the [Web Key Directory](https://datatracker.ietf.org/doc/draft-koch-openpgp-webkey-service/), where a domain serves its users' OpenPGP keys over HTTPS; [keys.openpgp.org](https://keys.openpgp.org), which publishes a key only after verifying the owner's address by email; classic HKP key servers; and DNS itself, through OPENPGPKEY ([RFC 7929](https://www.rfc-editor.org/rfc/rfc7929)) and SMIMEA ([RFC 8162](https://www.rfc-editor.org/rfc/rfc8162)) records protected by DNSSEC. Followed keys are refreshed on a schedule, so a rotated or revoked key is picked up automatically, or held for approval when the change cannot be verified.
|
||||
|
||||
Finally, Stalwart can [learn keys from the mail it receives](/docs/encryption/harvesting). When a correspondent sends a message signed with S/MIME, or one carrying an [Autocrypt](https://autocrypt.org/) header with their OpenPGP key, the server stores the key, but only if the message passed DMARC, was not classified as spam and the key belongs to the sender's address. From then on, replies from your organisation to that person can be encrypted. An Autocrypt key whose message signature could not be checked, or a self-signed certificate, is stored at the lowest trust level, which the gateway does not use by default.
|
||||
|
||||
Every key carries a trust level that reflects where it came from. At the top are keys an administrator entered or vouched for; then keys proven by a certificate authority or DNSSEC; then keys fetched from a directory such as WKD or keys.openpgp.org, and keys learned from mail; and at the bottom, keys nobody can vouch for. The gateway can demand a minimum level per recipient. When a key changes in a way the server cannot verify on its own, the new key waits for an administrator's approval while the old one stays in use. S/MIME certificates are validated against the root certificates that Mozilla trusts for email, or against your own private certificate authorities, and harvested certificates are checked against revocation lists every day.
|
||||
|
||||
What Stalwart never does is look up a key for an address it has no record of. Searching a key server at the moment a message is sent would tell a third party who your users write to, and would delay every message to someone without a key. Keys exist only for the addresses you, or your correspondents' own signed mail, have told the server about.
|
||||
|
||||
## Every algorithm that matters
|
||||
|
||||
Underneath all of this sits a new cryptographic engine, written from scratch in Rust for Stalwart. It replaces the third-party libraries we used before, removes more than 80 dependencies from the server, and was built for one job: encrypting mail quickly and correctly for whatever key it is handed. Its output is tested against independent implementations, including GnuPG (both gpg and gpgsm), OpenSSL, Mozilla's NSS, RNP (the engine inside Thunderbird), Sequoia, rPGP, OpenPGP.js, Proton's GopenPGP, PGPainless and Bouncy Castle, so that the format it picks for each key is one that the key's owner can read.
|
||||
|
||||
On the OpenPGP side, Stalwart implements [RFC 9580](https://www.rfc-editor.org/rfc/rfc9580), the 2024 revision of the standard, alongside the formats every existing client understands. It accepts both version 4 keys, which is what nearly all software creates today, and the new version 6 keys. For the public-key part it supports **RSA**, the long-standing workhorse, from 2048 to 8192 bits; **elliptic-curve Diffie-Hellman** on the NIST P-256, P-384 and P-521 curves; and **X25519** and **X448**, the fast modern curves designed by Daniel J. Bernstein and Mike Hamburg respectively, including the legacy Curve25519 format that GnuPG creates. Messages are encrypted with **AES**, either in the classic format every client reads or, when the recipient's key says its software supports it, in the newer format of RFC 9580, whose authenticated encryption (AES in **OCB** or **GCM** mode) detects any tampering with the message.
|
||||
|
||||
On the S/MIME side, Stalwart writes the Cryptographic Message Syntax used by every S/MIME client. It encrypts for **RSA** certificates, with either the classic padding every client reads or its modern, provably secure successor **RSA-OAEP**, for elliptic-curve certificates on the **NIST P-256, P-384 and P-521** curves, and for **X25519** and **X448** certificates. The content itself is protected with **AES-256-CBC**, which every client reads, or with **AES-256-GCM**, the authenticated encryption that the current S/MIME standard recommends. Stalwart knows which mail clients can read what, from Outlook's particular expectations for elliptic-curve keys to Apple Mail's limitations, and defaults to the choice that works with the most clients.
|
||||
|
||||
### Post-quantum encryption
|
||||
|
||||
Every public-key algorithm above relies on mathematical problems that today's computers cannot solve in any useful amount of time: factoring large numbers for RSA, and discrete logarithms on elliptic curves for the rest. A sufficiently large quantum computer could solve both. No such machine exists yet, and nobody knows when one will. But encrypted email has a long shelf life, and an adversary who records encrypted traffic today can afford to wait. This strategy is known as "harvest now, decrypt later", and it is the reason post-quantum cryptography matters for messages being sent right now, not only for those that will be sent once quantum computers arrive.
|
||||
|
||||
Post-quantum algorithms are built on different mathematics, which resists attacks by both classical and quantum computers. The first standardised for encryption is **ML-KEM** (Module-Lattice-based Key Encapsulation Mechanism), published by NIST in 2024 as FIPS 203 after a public competition that ran for most of a decade, in which it was known as Kyber. Stalwart supports it in both email standards:
|
||||
|
||||
In OpenPGP, Stalwart implements [RFC 9980](https://www.rfc-editor.org/rfc/rfc9980), which pairs ML-KEM with a classical curve: **ML-KEM-768 with X25519**, and **ML-KEM-1024 with X448**. The pairing is deliberate. An attacker must break both algorithms to read the message, so the encryption is at least as strong as the classical curve alone, even if a flaw were ever found in the newer lattice mathematics. Messages for post-quantum keys are always encrypted with AES-256.
|
||||
|
||||
In S/MIME, Stalwart implements [RFC 9936](https://www.rfc-editor.org/rfc/rfc9936), encrypting for certificates with **ML-KEM-512**, **ML-KEM-768** and **ML-KEM-1024** keys using the key encapsulation format of [RFC 9629](https://www.rfc-editor.org/rfc/rfc9629), with AES-256-GCM for the content.
|
||||
|
||||
Using it requires no configuration. When a user registers a key with a post-quantum component, encryption at rest uses it; when a recipient publishes one, the gateway does the same. Stalwart is also careful about one subtle trap: a key that contains both a classical and a post-quantum encryption key is encrypted to the post-quantum one only, because encrypting to both would let anyone who breaks the classical half read the message.
|
||||
|
||||
A word of honesty: mail clients are still catching up. Thunderbird and GnuPG in its default configuration cannot yet decrypt post-quantum OpenPGP messages, while clients built on Sequoia, rPGP, OpenPGP.js or GopenPGP can. On the S/MIME side, no mainstream mail client decrypts ML-KEM yet, and no public certificate authority issues ML-KEM certificates, so post-quantum keys are for now best suited to users whose software is ready for them. The server side is ready today, and when the clients arrive, nothing on your server will need to change.
|
||||
|
||||
## Who holds the key
|
||||
|
||||
Almost every mail service says it encrypts your mail, and almost every one of them does. The difference lies in a single question: who holds the key that opens the stored messages?
|
||||
|
||||
For the large hosted services, the answer is the provider. Microsoft 365 encrypts mailboxes at rest, but by default "[Microsoft manages all cryptographic keys](https://learn.microsoft.com/en-us/purview/microsoft-365-service-encryption)", and even when a customer supplies its own root keys, it [authorises the service](https://learn.microsoft.com/en-us/purview/customer-key-overview) to keep using them for search, anti-spam and eDiscovery. Google, Apple's iCloud Mail works the same way by default: storage encryption protects against a stolen disk, but the provider holds the keys. Google does offer [client-side encryption](https://knowledge.workspace.google.com/admin/security/about-client-side-encryption) that its servers cannot decrypt, but only on selected Enterprise and Education editions, with the keys held by an external key service the organisation chooses rather than by each user. Apple [says plainly](https://support.apple.com/102651) that iCloud Mail is not end-to-end encrypted, because it has to interoperate with the rest of the email world.
|
||||
|
||||
Self-hosted servers have mostly followed a similar pattern, apart from a few add-on tools that encrypt incoming mail with the recipient's public key. Dovecot's [mail-crypt plugin](https://doc.dovecot.org/main/core/plugins/mail_crypt.html) encrypts stored mail either with a global key kept in the server's configuration or with per-user keys stored on the server, unprotected or protected by a value derived from the user's password, and its own documentation describes per-folder keys as "not considered production quality". In every variant the private keys are kept on the server, which decrypts mail itself to serve it over IMAP, and the format is its own rather than OpenPGP or S/MIME.
|
||||
|
||||
Privacy-focused providers such as Proton Mail and Tuta go much further. Their users' private keys are kept on the provider's servers, but [encrypted with a key derived from the user's password](https://proton.me/support/how-is-the-private-key-stored), and they are unlocked only inside the provider's app or web client at login. Between two users of the same service, mail is end-to-end encrypted, which is a stronger guarantee than any server-side scheme can offer. The protection of stored mail, however, rests on the password and on the client code the provider delivers, and Tuta's protocol cannot be read by standard OpenPGP or S/MIME software at all.
|
||||
|
||||
Stalwart builds that model into the server itself. The private key does not exist on the server in any form, not even encrypted. Someone holding a complete copy of the server, its database, its backups and the administrator's password still cannot read a single encrypted message, because the only key that opens them lives in the user's own mail client. And because the messages are standard OpenPGP and S/MIME, they can be read by any client that supports those standards, and they stay readable if the mailbox is ever moved elsewhere. A few providers, such as Posteo and mailbox.org, offer a similar option of encrypting incoming mail with the user's own public key; Stalwart brings the same model to any server you run yourself, and adds the gateway, key discovery, header protection and post-quantum algorithms on top. Post-quantum support is still uncommon. Proton, which co-authored RFC 9980, added opt-in post-quantum OpenPGP keys this year; Tuta's TutaCrypt uses a protocol of its own; and Dovecot supports ML-KEM keys in its own storage format. Stalwart implements the published IETF standards for both OpenPGP and S/MIME. Unlike services that publish only their client apps, the whole of Stalwart, server included, is open source.
|
||||
|
||||
Honesty requires a few qualifications, and we would rather state them than have you discover them. Encryption at rest protects mail from the moment it is stored, not before: the server sees each incoming message in readable form when it arrives, which is what lets the spam filter and Sieve scripts do their work, and an attacker in control of the running server at that moment could read it too. Unless the sender encrypted the message, the protection is not end-to-end. Header fields, the subject included, stay readable, so that clients can list and sort mail, and the server can search only those headers, not the encrypted bodies. Encrypted messages can be read only in a client that supports OpenPGP or S/MIME, not in a webmail without it. Some messages stay in plain text: mail stored before encryption was enabled, drafts and sent copies that clients upload unless the user also turns on encryption of uploaded messages (JMAP drafts, and the sent copies of messages submitted over JMAP, always), and new mail while the registered key is expired or unusable. And a user who loses their private key loses access to the mail encrypted for it, because nobody, Stalwart included, can recover it. And a user who loses their private key loses access to the mail encrypted for it, because nobody, Stalwart included, can recover it.
|
||||
|
||||
## Upgrading
|
||||
|
||||
Stalwart v1.0.2 is available now. Upgrading is a matter of replacing the binary or pulling the new container image, as described in the [upgrade instructions](/docs/install/upgrade). The [changelog](https://github.com/stalwartlabs/stalwart/blob/main/CHANGELOG.md) lists every change in this release.
|
||||
|
||||
:::caution[Encryption at rest must be enabled again]
|
||||
Because the cipher is now chosen from each user's key, accounts that had encryption at rest enabled are set to disabled during the upgrade, and new mail for them is stored unencrypted until the key is selected again in the account's encryption settings. Registered keys and messages that are already encrypted are not affected. The server-wide switch has also moved to the new Encryption settings, where it is enabled by default, so a server that had turned encryption at rest off must turn it off again; and encryption of client-uploaded messages is now a per-account setting. The [encryption at rest documentation](/docs/encryption/at-rest#enable-encryption-at-rest) describes the steps.
|
||||
:::
|
||||
|
||||
## One last thing
|
||||
|
||||
While the server team was busy with ciphers, the WebUI got a makeover, and it now has a good deal more pizzazz. Version 2.0.4 brings a new colour palette and a reorganised sidebar, and every settings form has been reworked: fields sit in two columns on wide screens and stack neatly on a phone, nested settings are simpler to follow, variants are picked from segmented controls, long forms get an index of their sections, and the save button floats along as you scroll. The dashboards were rebuilt too, as a mosaic of statistics, charts, headline figures and rankings, so the state of the server reads at a glance, in the light theme or the dark one.
|
||||
|
||||
<Image src={dashboardLight} alt="The restyled Stalwart WebUI dashboard, with tiles for received, sent and pending messages and server memory, a chart of messages over the last 24 hours, the share of clean mail and a ranking of rejections" class="dark:sl-hidden" />
|
||||
<Image src={dashboardDark} alt="The restyled Stalwart WebUI dashboard in the dark theme, with tiles for received, sent and pending messages and server memory, a chart of messages over the last 24 hours, the share of clean mail and a ranking of rejections" class="light:sl-hidden" />
|
||||
|
||||
To try it, run the [Update Applications](/docs/management/applications/update#update-applications-now) action from the WebUI, or with `stalwart-cli create Action/UpdateApps`, and the server downloads WebUI 2.0.4 straight away.
|
||||
|
||||
## Stay tuned
|
||||
|
||||
Encryption is only the first of the features that were held back from the v1.0 cut. Several more are finished or nearly so, and they will arrive over the coming weeks, one release at a time. Like everything in this post, all of them will be open source under the AGPL. Stay tuned.
|
||||
@@ -10,7 +10,7 @@ Groups are also a convenient target for sharing. Anything shared with the group
|
||||
|
||||
A group is not a distribution list. Mail sent to the group's address is delivered to the group's own mailbox, not to each member. To distribute each message to a list of recipients, use a [mailing list](/docs/email/management/mailing-lists) instead.
|
||||
|
||||
A group is the `Group` variant of the [Account](/docs/ref/object/account) object (found in the WebUI under <!-- breadcrumb:Account --><svg class="lucide-icon" width="1em" height="1em" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" ><rect width="20" height="14" x="2" y="3" rx="2" /><line x1="8" x2="16" y1="21" y2="21" /><line x1="12" x2="12" y1="17" y2="21" /></svg> Management › <svg class="lucide-icon" width="1em" height="1em" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" ><path d="M16 21v-2a4 4 0 0 0-4-4H6a4 4 0 0 0-4 4v2" /><path d="M16 3.128a4 4 0 0 1 0 7.744" /><path d="M22 21v-2a4 4 0 0 0-3-3.87" /><circle cx="9" cy="7" r="4" /></svg> Directory › Groups, <svg class="lucide-icon" width="1em" height="1em" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" ><rect width="20" height="14" x="2" y="3" rx="2" /><line x1="8" x2="16" y1="21" y2="21" /><line x1="12" x2="12" y1="17" y2="21" /></svg> Management › <svg class="lucide-icon" width="1em" height="1em" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" ><path d="M16 21v-2a4 4 0 0 0-4-4H6a4 4 0 0 0-4 4v2" /><path d="M16 3.128a4 4 0 0 1 0 7.744" /><path d="M22 21v-2a4 4 0 0 0-3-3.87" /><circle cx="9" cy="7" r="4" /></svg> Directory › Accounts<!-- /breadcrumb:Account -->). The [reference page](/docs/ref/object/account) lists every field, and the sections below describe those that require a decision.
|
||||
A group is the `Group` variant of the [Account](/docs/ref/object/account) object (found in the WebUI under <!-- breadcrumb:Account/Group --><svg class="lucide-icon" width="1em" height="1em" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" ><rect width="20" height="14" x="2" y="3" rx="2" /><line x1="8" x2="16" y1="21" y2="21" /><line x1="12" x2="12" y1="17" y2="21" /></svg> Management › <svg class="lucide-icon" width="1em" height="1em" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" ><path d="M16 21v-2a4 4 0 0 0-4-4H6a4 4 0 0 0-4 4v2" /><path d="M16 3.128a4 4 0 0 1 0 7.744" /><path d="M22 21v-2a4 4 0 0 0-3-3.87" /><circle cx="9" cy="7" r="4" /></svg> Directory › Groups<!-- /breadcrumb:Account/Group -->). The [reference page](/docs/ref/object/account) lists every field, and the sections below describe those that require a decision.
|
||||
|
||||
## Address
|
||||
|
||||
|
||||
@@ -6,7 +6,7 @@ description: "The User variant of the Account object: address, credentials, alia
|
||||
|
||||
A user account represents someone who signs in to the server and owns a mailbox. Usually that is a person, but it can also be a service, such as an application that sends notifications. Creating a user account is what gives a new user an address, a place to store mail and data, and the credentials to reach them.
|
||||
|
||||
A user account is the `User` variant of the [Account](/docs/ref/object/account) object (found in the WebUI under <!-- breadcrumb:Account --><svg class="lucide-icon" width="1em" height="1em" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" ><rect width="20" height="14" x="2" y="3" rx="2" /><line x1="8" x2="16" y1="21" y2="21" /><line x1="12" x2="12" y1="17" y2="21" /></svg> Management › <svg class="lucide-icon" width="1em" height="1em" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" ><path d="M16 21v-2a4 4 0 0 0-4-4H6a4 4 0 0 0-4 4v2" /><path d="M16 3.128a4 4 0 0 1 0 7.744" /><path d="M22 21v-2a4 4 0 0 0-3-3.87" /><circle cx="9" cy="7" r="4" /></svg> Directory › Groups, <svg class="lucide-icon" width="1em" height="1em" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" ><rect width="20" height="14" x="2" y="3" rx="2" /><line x1="8" x2="16" y1="21" y2="21" /><line x1="12" x2="12" y1="17" y2="21" /></svg> Management › <svg class="lucide-icon" width="1em" height="1em" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" ><path d="M16 21v-2a4 4 0 0 0-4-4H6a4 4 0 0 0-4 4v2" /><path d="M16 3.128a4 4 0 0 1 0 7.744" /><path d="M22 21v-2a4 4 0 0 0-3-3.87" /><circle cx="9" cy="7" r="4" /></svg> Directory › Accounts<!-- /breadcrumb:Account -->). This page describes the fields that require a decision when an account is created or changed, grouped by what they decide. The [reference page](/docs/ref/object/account) lists every field with its type and default, and [Accounts](/docs/auth/accounts/) explains the model.
|
||||
A user account is the `User` variant of the [Account](/docs/ref/object/account) object (found in the WebUI under <!-- breadcrumb:Account/User --><svg class="lucide-icon" width="1em" height="1em" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" ><rect width="20" height="14" x="2" y="3" rx="2" /><line x1="8" x2="16" y1="21" y2="21" /><line x1="12" x2="12" y1="17" y2="21" /></svg> Management › <svg class="lucide-icon" width="1em" height="1em" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" ><path d="M16 21v-2a4 4 0 0 0-4-4H6a4 4 0 0 0-4 4v2" /><path d="M16 3.128a4 4 0 0 1 0 7.744" /><path d="M22 21v-2a4 4 0 0 0-3-3.87" /><circle cx="9" cy="7" r="4" /></svg> Directory › Accounts<!-- /breadcrumb:Account/User -->). This page describes the fields that require a decision when an account is created or changed, grouped by what they decide. The [reference page](/docs/ref/object/account) lists every field with its type and default, and [Accounts](/docs/auth/accounts/) explains the model.
|
||||
|
||||
## Identity and address
|
||||
|
||||
|
||||
@@ -63,4 +63,4 @@ Tenant administrators are not trusted with the server's network position. An obj
|
||||
- URLs configured on tenant-owned objects, such as DNS server endpoints, ACME directories and domain logos, must use https, must not contain credentials and must resolve to public IP addresses.
|
||||
- Secrets of tenant-owned objects must be given as values. The `File` and `EnvironmentVariable` sources, which would read from the server's file system or environment, are rejected.
|
||||
- OAuth clients owned by a tenant are shown as unverified on the consent screen, and they sign in only accounts of that tenant. Any other account receives `access_denied` ([OAuth clients](/docs/auth/oauth/clients#tenant-owned-clients)).
|
||||
- An OpenID Connect directory owned by a tenant contacts its identity provider only on public IP addresses ([OpenID Connect directory](/docs/auth/backend/oidc)).
|
||||
- An OpenID Connect directory owned by a tenant contacts its identity provider only on public IP addresses, unless a superuser has marked the directory as a trusted provider ([OpenID Connect directory](/docs/auth/backend/oidc)).
|
||||
@@ -32,7 +32,7 @@ Accounts are never deleted or suspended automatically. A user disabled at the pr
|
||||
|
||||
The `issuer` in the metadata must equal `issuerUrl` exactly, and a trailing slash counts. Type the issuer as the provider publishes it, or enable `lenientIssuerMatch`. Templated multi-tenant issuers that contain a `{tenantid}` placeholder are not supported, so use the tenant-specific issuer URL instead.
|
||||
|
||||
Requests to the provider time out after 30 seconds and never follow redirects. Which providers a directory may contact depends on its owner. A directory owned by the server may contact providers on private networks, whereas one owned by a [tenant](/docs/auth/authorization/tenants) may contact only providers on public addresses. [`allowInvalidCerts`](/docs/ref/object/directory#allowinvalidcerts) accepts invalid TLS certificates and is meant for testing.
|
||||
Requests to the provider time out after 30 seconds and never follow redirects. Which providers a directory may contact depends on its owner. A directory owned by the server may contact providers on private networks, whereas one owned by a [tenant](/docs/auth/authorization/tenants) may contact only providers on public addresses. A superuser can lift this restriction for a tenant's directory by enabling [`trustedProvider`](/docs/ref/object/directory#trustedprovider), which makes sense when the tenant's provider is an internal service that the operator vouches for. The directory then contacts its provider as a server-owned directory does. Because a superuser vouched for that particular provider, the server refuses any change to a trusted directory that does not come from a superuser, so a tenant administrator cannot point it at another address. [`allowInvalidCerts`](/docs/ref/object/directory#allowinvalidcerts) accepts invalid TLS certificates and is meant for testing.
|
||||
|
||||
Temporary failures while the server starts are retried for a short time. Three conditions make the directory fail to load, and the error is logged: a discovery failure, an issuer mismatch, and `introspection` mode with a provider that publishes no introspection endpoint.
|
||||
|
||||
@@ -116,7 +116,7 @@ The provider has to know the WebUI as a client, and the client id the WebUI pres
|
||||
There is no built-in client id. If no static id is set and the provider does not advertise `client_id_metadata_document_supported` in its metadata, the WebUI shows a configuration error.
|
||||
|
||||
:::note
|
||||
If the provider does not support Client ID Metadata Documents, register a public client for the WebUI at the provider and set its client id as the Application's `oauthClientId`. Set it only in that case, because the WebUI then uses the static id for every sign-in. On a server where some domains sign in through Stalwart's own authorization server, that server answers `Unknown client id` unless a first-party [OAuthClient](/docs/auth/oauth/clients) with the same client id exists.
|
||||
If the provider does not support Client ID Metadata Documents, register a public client for the WebUI at the provider and set its client id as the Application's `oauthClientId`. Set it only in that case, because the WebUI then uses the static id for every sign-in. On a server where some domains sign in through Stalwart's own authorization server, that server answers `Unknown client id` unless a first-party [OAuthClient](/docs/auth/oauth/clients) with the same client id exists. That client lists the same redirect URIs as the [provider checklist](#provider-checklist) and has no secret. Create it before setting `oauthClientId`, so that the recovery administrator can still sign in.
|
||||
:::
|
||||
|
||||
### Scopes requested
|
||||
|
||||
@@ -58,7 +58,7 @@ A version 1 header, or a version 2 header without the TLV, makes the connection
|
||||
|
||||
A token used on IMAP, POP3, SMTP or ManageSieve must carry the `urn:ietf:params:oauth:scope:mail` scope. API keys have no scopes, so they are not subject to this check.
|
||||
|
||||
When a bearer mechanism fails, the server does not simply reject the attempt. It answers as RFC 7628 sections 3.2.2 and 3.2.3 describe, in three steps:
|
||||
When a bearer mechanism fails, the server does not simply reject the attempt. It answers as RFC 7628 describes, in three steps:
|
||||
|
||||
1. The server sends a JSON error document as a SASL challenge, which tells the client what went wrong.
|
||||
2. The client replies with a single `%x01` byte (`AQ==` in base64) to acknowledge it.
|
||||
|
||||
@@ -50,7 +50,7 @@ The server answers with `201 Created`. The response contains the new client id a
|
||||
|
||||
The server checks each registration against the OAuth Profile for Open Public Clients. A request that breaks a rule is answered with `400 Bad Request` and an RFC 7591 error. The rules are:
|
||||
|
||||
- `redirect_uris` must not be empty. Each entry must be either a loopback URI with a path, `http://127.0.0.1[:port]/<path>` or `http://[::1][:port]/<path>`, or a URI with a private-use scheme that contains a dot, such as `com.example.mail:/oauth`. A port given at registration is accepted, and any port matches at authorization time, as [RFC 8252](https://www.rfc-editor.org/rfc/rfc8252) section 7.3 requires for native applications. The following are refused with `invalid_redirect_uri`: https URIs, `http://localhost`, other `http://` hosts, schemes without a dot, URIs with a fragment, and a loopback URI without a path (`http://127.0.0.1`).
|
||||
- `redirect_uris` must not be empty. Each entry must be either a loopback URI with a path, `http://127.0.0.1[:port]/<path>` or `http://[::1][:port]/<path>`, or a URI with a private-use scheme that contains a dot, such as `com.example.mail:/oauth`. A port given at registration is accepted, and any port matches at authorization time, as [RFC 8252](https://www.rfc-editor.org/rfc/rfc8252) requires for native applications. The following are refused with `invalid_redirect_uri`: https URIs, `http://localhost`, other `http://` hosts, schemes without a dot, URIs with a fragment, and a loopback URI without a path (`http://127.0.0.1`).
|
||||
- `token_endpoint_auth_method` must be `none` or absent, because a dynamically registered client never has a secret.
|
||||
- `grant_types`, when present, must include both `authorization_code` and `refresh_token`. `response_types`, when present, must include `code`. The device grant cannot be registered this way.
|
||||
- `client_uri`, `logo_uri`, `tos_uri` and `policy_uri` must be https URLs.
|
||||
|
||||
@@ -118,7 +118,7 @@ curl -X POST https://mail.example.com/auth/revoke \
|
||||
|
||||
A successful revocation deletes the whole grant, with the same effect as deleting the application in Connected Applications: every access and refresh token of the grant stops working. The response is `200 OK` with an empty body.
|
||||
|
||||
The same response is returned for a token that cannot be decoded, belongs to another client or has no grant, as RFC 7009 section 2.2 requires. The response therefore reveals nothing about tokens the caller does not own. Two kinds of request are refused:
|
||||
The same response is returned for a token that cannot be decoded, belongs to another client or has no grant, as RFC 7009 requires. The response therefore reveals nothing about tokens the caller does not own. Two kinds of request are refused:
|
||||
|
||||
- A request without `token`, or with Basic credentials and a different client in the body, is refused with `invalid_request`.
|
||||
- A request without a `client_id`, or whose client authentication fails, is refused with `invalid_client`.
|
||||
|
||||
@@ -110,7 +110,7 @@ Two optional restrictions need maintenance over time. [`allowedIps`](/docs/ref/o
|
||||
|
||||
## Restricting network access
|
||||
|
||||
The SCIM endpoint is served on the same HTTP listeners as the other services, so by default it is reachable wherever they are. When the identity provider connects from known addresses, the endpoint can be confined to them. The [`allowedEndpoints`](/docs/ref/object/http#allowedendpoints) expression of the [Http](/docs/ref/object/http) singleton does this. The singleton is found in the WebUI under <!-- breadcrumb:Http --><svg class="lucide-icon" width="1em" height="1em" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" ><path d="M10 5H3" /><path d="M12 19H3" /><path d="M14 3v4" /><path d="M16 17v4" /><path d="M21 12h-9" /><path d="M21 19h-5" /><path d="M21 5h-7" /><path d="M8 10v4" /><path d="M8 12H3" /></svg> Settings › <svg class="lucide-icon" width="1em" height="1em" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" ><path d="M17 19a1 1 0 0 1-1-1v-2a2 2 0 0 1 2-2h2a2 2 0 0 1 2 2v2a1 1 0 0 1-1 1z" /><path d="M17 21v-2" /><path d="M19 14V6.5a1 1 0 0 0-7 0v11a1 1 0 0 1-7 0V10" /><path d="M21 21v-2" /><path d="M3 5V3" /><path d="M4 10a2 2 0 0 1-2-2V6a1 1 0 0 1 1-1h4a1 1 0 0 1 1 1v2a2 2 0 0 1-2 2z" /><path d="M7 5V3" /></svg> Network › HTTP › General, <svg class="lucide-icon" width="1em" height="1em" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" ><path d="M10 5H3" /><path d="M12 19H3" /><path d="M14 3v4" /><path d="M16 17v4" /><path d="M21 12h-9" /><path d="M21 19h-5" /><path d="M21 5h-7" /><path d="M8 10v4" /><path d="M8 12H3" /></svg> Settings › <svg class="lucide-icon" width="1em" height="1em" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" ><path d="M17 19a1 1 0 0 1-1-1v-2a2 2 0 0 1 2-2h2a2 2 0 0 1 2 2v2a1 1 0 0 1-1 1z" /><path d="M17 21v-2" /><path d="M19 14V6.5a1 1 0 0 0-7 0v11a1 1 0 0 1-7 0V10" /><path d="M21 21v-2" /><path d="M3 5V3" /><path d="M4 10a2 2 0 0 1-2-2V6a1 1 0 0 1 1-1h4a1 1 0 0 1 1 1v2a2 2 0 0 1-2 2z" /><path d="M7 5V3" /></svg> Network › HTTP › Security<!-- /breadcrumb:Http -->.
|
||||
The SCIM endpoint is served on the same HTTP listeners as the other services, so by default it is reachable wherever they are. When the identity provider connects from known addresses, the endpoint can be confined to them. The [`allowedEndpoints`](/docs/ref/object/http#allowedendpoints) expression of the [Http](/docs/ref/object/http) singleton does this. The singleton is found in the WebUI under <!-- breadcrumb:Http/HttpSecurity --><svg class="lucide-icon" width="1em" height="1em" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" ><path d="M10 5H3" /><path d="M12 19H3" /><path d="M14 3v4" /><path d="M16 17v4" /><path d="M21 12h-9" /><path d="M21 19h-5" /><path d="M21 5h-7" /><path d="M8 10v4" /><path d="M8 12H3" /></svg> Settings › <svg class="lucide-icon" width="1em" height="1em" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" ><path d="M17 19a1 1 0 0 1-1-1v-2a2 2 0 0 1 2-2h2a2 2 0 0 1 2 2v2a1 1 0 0 1-1 1z" /><path d="M17 21v-2" /><path d="M19 14V6.5a1 1 0 0 0-7 0v11a1 1 0 0 1-7 0V10" /><path d="M21 21v-2" /><path d="M3 5V3" /><path d="M4 10a2 2 0 0 1-2-2V6a1 1 0 0 1 1-1h4a1 1 0 0 1 1 1v2a2 2 0 0 1-2 2z" /><path d="M7 5V3" /></svg> Network › HTTP › Security<!-- /breadcrumb:Http/HttpSecurity -->.
|
||||
|
||||
The following expression answers `404` to SCIM requests from any address outside the provider's range:
|
||||
|
||||
|
||||