Email notifications
ssoosshd can email people when something happens to a credential they hold.
It is off by default and adds nothing to the certificate flows: every path
that emits a notification behaves identically with mail disabled.
Nothing a browser or an unattended job waits on ever waits on the mail relay. An approval or a redemption publishes an event to the internal queue and returns; rendering and SMTP happen on a background consumer. A relay that is slow, greylisting, or down delays no certificate work – it delays only the notification.
What is never emailed
Section titled “What is never emailed”The “service code created” notification carries what the recipient needs to
recognise the code – the service account, when it expires, who approved it and
from where, and at the advanced level the bound key’s fingerprint – so the
message is actionable without being redeemable. The ssh_config recipe stays in
the terminal, which prints it with the real code and key path; a copy in mail
could only carry placeholders. There is a test asserting the code does not appear in a rendered
message, and another asserting it never reaches the notification payload; both
cover every registered kind, so adding a field later is a decision rather than
an accident.
The rule holds for the two later enrollment kinds too. The expiry reminder
says to run ssoossh service enroll again rather than offering anything to
reuse, and the expired-attempt report names the enrollment without naming the
code that was presented.
What a “was this you?” message has to answer
Section titled “What a “was this you?” message has to answer”The three *_certificate_issued kinds exist for one reader: the person
deciding whether a certificate in their name was theirs. “A certificate was
issued” is not answerable. What is answerable is what asked and who let it
happen, so every message carries both.
What asked is the part of the host context a person recognises: the account and machine, who invoked it, and the address the request came from. A console login also always shows its terminal and, when there is one, the remote host it reports. At the advanced level the command line and terminal follow. The rest of the host context – the machine’s stable id, its platform, the client implementation and version, the process ids – stays on the certificate’s page, which the message links to; see Host context for where each value comes from.
Who let it happen is the approval, and the fact that answers it is the address it was approved from (the browser follows at the advanced level). The approver is the recipient, so the message does not name them back to themselves. This is the part that distinguishes the two bad outcomes from each other. A certificate you do not recognize approved from an address you were at is a machine you should look at; the same certificate approved from an address you have never used is an account someone else was driving.
service_enrollment_created names the approver, because its recipients are the
account’s holders rather than the approver alone, and the address they
approved from: it announces a credential that mints certificates unattended
for as long as the code lives. RequestSourceIP there (advanced level) is the
requester’s address and ApproverSourceIP the approver’s – two different
questions that are easy to conflate.
Each message leads with one sentence of what happened, one line of what to do if it was not you, and one link to the page where you act on it: the certificate, or the service code, whose page has the expire control.
Configuring the relay
Section titled “Configuring the relay”Two deployments are worth writing out. A local relay, where the mail system already on this host takes it from here: no TLS and no authentication are reasonable, because the connection never leaves the machine.
mail: enabled: true from: "ssoossh <no-reply@example.com>" smtp: host: "localhost" port: 25 tls: "off" auth: "none"A remote submission service, where the connection crosses a network, so both TLS and authentication are strongly suggested:
mail: enabled: true from: "ssoossh <no-reply@example.com>" reply_to: "ssh-admins@example.com" subject_prefix: "[ssoossh] " smtp: host: "smtp.example.com" port: 587 tls: "required" auth: "plain" username: "ssoossh@example.com" password_file: "/run/secrets/ssoossh-smtp-password"| Key | Default | Notes |
|---|---|---|
mail.enabled |
false |
with it false the notification handler is not registered at all, so no event is queued and nothing is rendered |
mail.from |
empty | the envelope and header sender. Required when enabled |
mail.reply_to |
unset | worth setting to a monitored address: these messages report credential activity, and the first thing a recipient does with an unexpected one is reply |
mail.subject_prefix |
empty | prepended to every rendered subject, so you can tag messages without editing a template |
mail.template_dir |
unset | local template overrides; see below |
mail.smtp.host |
localhost |
required when mail is enabled |
mail.smtp.port |
25 |
conventionally 25 local, 587 submission with STARTTLS, 465 implicit TLS |
mail.smtp.helo |
unset | empty lets the library use the local hostname, which is right unless the relay checks it |
mail.smtp.timeout |
15s |
bounds one delivery’s connect-and-send. It bounds only the background sender, never a request |
TLS and authentication are optional, and suggested
Section titled “TLS and authentication are optional, and suggested”Neither is mandatory: the local-relay deployment legitimately needs neither,
and forcing them would make the simplest working setup impossible. But the
server warns at startup when it is relaying to a non-loopback host with
tls: "off" or
tls: "opportunistic" (which anyone on the path can strip), with
auth: "none", or with
insecure_skip_verify: true.
Those warnings are the suggestion; they are not errors, and nothing fails
because of them.
mail.smtp.tls accepts:
| Value | Meaning |
|---|---|
off |
Never negotiate STARTTLS. Reasonable only for a loopback relay |
opportunistic |
Use STARTTLS when offered, plaintext when not. The default |
required |
Fail the delivery rather than send in plaintext |
implicit |
Dial TLS directly (SMTPS, conventionally port 465) |
mail.smtp.auth accepts none
(the default), auto, plain, login, cram-md5, scram-sha-1,
scram-sha-256, and xoauth2. auto negotiates the strongest mechanism the
relay advertises.
mail.smtp.username is
required for every mechanism but none.
Two more keys cover a relay with a private CA or a name that does not match:
mail.smtp.ca_file (empty
uses the system trust store) and
mail.smtp.server_name
(needed only when host is an address rather than the name on the
certificate).
Prefer
mail.smtp.password_file
to mail.smtp.password: it
keeps the secret out of the config file, and out of the effective
configuration auditors can read. It takes precedence when both are set, is
read once at startup, and has trailing whitespace trimmed, since an editor’s
newline is never part of a password.
Anything wrong here fails the server at startup rather than at first delivery
– an unparseable from, an unreadable password file, a template that does
not compile. A notification that never arrives looks exactly like one that was
never triggered, which is the worst thing this feature could do quietly.
Delivery logs route separately, by a type=mail attribute like the LDAP and
audit logs: mail.logging.level
(default info) sets the threshold for what both halves of the mail path say,
and setting a
filename moves those
records out of the general log into their own rotating file.
Multi-instance deployments
Section titled “Multi-instance deployments”The delivery consumer joins a queue group, so exactly one instance sends each
notification. Without that, a recipient would get one copy per running server.
Nothing else about a multi-instance deployment needs configuring for mail –
see Multi-instance and NATS for the
notification.send subject.
The queue group deduplicates consumption, not publication, which matters
for the two notifications not emitted from a single request path. The expiry
reminder is found by a sweep every instance runs, and the expired-attempt
report can be raised by whichever instance fields a retry. Both therefore
claim their send in the database with a guarded UPDATE and publish only if
that claims a row, so two instances produce one message between them rather
than one each.
The two scheduled and rate-limited kinds
Section titled “The two scheduled and rate-limited kinds”mail: # How far ahead of an enrollment code's expiry the reminders start. # 0 disables them and the sweep is not registered at all. expiry_reminder_lead: 168h # At most one "expired code used" message per enrollment per window. # 0 disables that notification. expired_attempt_window: 24hmail.expiry_reminder_lead
opens the window; inside it the reminder repeats weekly until the code is
within its final week, then daily until it expires. A 30-day lead sends at 30,
23, 16 and 9 days out and then every day from 7 days out; the default week
sends the daily ones alone. Each send is claimed in the database, so a
multi-instance deployment sends each one once. Shortening the lead means a
code now past the new window gets nothing until it re-enters it; lengthening
it picks codes up at the next sweep. The sweep runs hourly (a lead shorter
than a day sweeps at a fraction of it), and skips codes that have already
expired – a reminder that something expired yesterday helps nobody, and the
aftermath has its own notification.
mail.expired_attempt_window
is a rate limit rather than a one-shot, because what it reports is a retry
loop: a cron job holding a dead code fails on its own schedule indefinitely,
and the recipient needs to keep hearing that it is still happening without
hearing it every five minutes.
Where an enrollment’s notifications go
Section titled “Where an enrollment’s notifications go”Notifications about a service enrollment – created, redeemed, expiring, and expired-code-used – go to every holder of its service account by default, resolved fresh at delivery from the accounts each user held at their last login.
That default leaves two gaps, and an optional per-enrollment notification address closes both:
- A service account whose holders have never logged in has no rows to fan out to, so the notification reaches nobody.
- An identity provider that releases no email claim silences every holder’s copy, and a large holder set turns every redemption into a mailshot where a team alias would do.
Set the address on the browser approval page when approving the request, or afterwards from the code’s own page under Service codes (any holder) or under Admin → Service codes (SOC). Clearing it restores fan-out.
A set address is the sole recipient and is sent ungated: with no single owning user there is no principled per-kind preference that could gate it, and the address is the account’s own subscription, entered deliberately. A holder who has opted out of a kind still does not get their own copy – they simply are not a recipient while an address is set.
There is deliberately no domain allowlist. Anyone who can set the address can
already approve certificates for the account. Changing it is recorded in the
audit stream as enrollment.notification_email_set, carrying both the old and
the new value, because redirecting a credential’s mail is exactly the kind of
quiet change an auditor wants to be able to find.
What users control
Section titled “What users control”Each user chooses which notifications they receive at /preferences in the
web UI. In a service-account fan-out each holder’s own choice gates their own
copy, so one person opting out silences nothing for anyone else, and an
account whose holders have all opted out sends nothing at all.
Choices are stored per (user, kind); a user who has never answered gets the kind’s own default, which is what lets a new notification ship without a backfill.
The three “was this you?” kinds – user_certificate_issued,
pam_certificate_issued and console_certificate_issued – default off,
and are the only ones that do. An interactive certificate is issued per login,
a PAM certificate per sudo and a console certificate per console login, so
any of them defaulting on would make every existing deployment noisy the day
it upgrades. They are separate kinds rather than one with a type field so a
user who runs sudo forty times a day and logs in twice can keep the login
signal without drowning in the other. A security-sensitive deployment can tell
its users to turn them on; nothing turns them on for them. The console kind is
the one such a deployment should push hardest: a console request is made by an
unauthenticated machine and approved by a code typed into the owner’s web
session, so a message the owner does not recognize means that session was used
by someone else.
The preference is read at delivery rather than at publication, so a notification queued moments before someone opts out is not delivered anyway.
One kind is not a preference at all. admin_auto_disabled tells an admin the
directory sync has disabled their account, and an admin in that position has
no session left to find out any other way. It is always sent: the
preferences page does not list it, a request to turn it off is refused, and it
arrives even when every other kind is off. It still honours the reader’s email
detail level. Only mail.enabled
being false, or an account with no address, stops it. See
When the sync disables an admin
for who counts as an admin.
Email detail level
Section titled “Email detail level”Each user also chooses how much every message carries, on the same page: Simplified or Advanced. Simplified is the sentence, the link, and the few facts a person checks: when it stops being valid, where it was requested and approved from. Advanced adds a Details section with key IDs, serials, certificate options, the bound key, and the claimed command and terminal.
The level is the user’s own, stored on their account, and separate from the web UI’s Simplified / Advanced setting. It is set once, at the user’s first login after it existed: Advanced for members of the SOC or admin group, Simplified for everyone else. A later change of groups does not move it. A user who has not logged in since, and an enrollment’s notification address, which has no account behind it, get Simplified. A fan-out renders each level once and sends each holder their own.
Two things stop delivery regardless of the toggle, and the page says so rather
than leaving the user to work it out:
mail.enabled being false, and an
identity whose provider releases no email claim.
Directory id changes, for admins
Section titled “Directory id changes, for admins”When the directory gives a stored NSS account or group a different uid or gid, hosts keep the stored number until an admin accepts the change under Admin > Directory (see Frozen uid and gid). Two kinds tell the admins about it:
nss_id_change_pendinglists what is new: one message per admin for everything a sync run made pending, sent when the run ends, and one for whatever on-demand and login reads made pending, sent by a sweep every five minutes. Each change is mailed once. A change accepted, or reverted in the directory, before the message goes out is not mailed, and a different new number for the same entry is mailed again as new.nss_id_change_reminderlists everything still waiting, with how long each has waited, every Monday at 09:00 in the server’s local time zone, and only while something is waiting.
Both go to every admin: each user whose groups at their last login include
admin.require_group, who
has an email address, and who is not disabled. There is no address list to
configure. Because the groups are the ones the last login recorded, someone
removed from the admin group at the identity provider keeps getting these
until they log in again, and a new admin gets them from their first login. A
change that waits while no admin can be mailed is mailed as new once one can.
Both are preferences that default on, listed on the preferences page only to
a session holding the admin role, so each admin can switch either off. Each
message lists at most 50 changes, oldest first, says how many more there are,
and links to the pending changes on the directory page. Every instance runs
the sweep and the reminder; each change is claimed on its row and each week’s
reminder on nss_alerts, so a restart or a second instance never repeats a
message.
Notification kinds
Section titled “Notification kinds”Sixteen kinds. Every one ships three templates, named
<kind>.subject.tmpl, <kind>.txt.tmpl and <kind>.html.tmpl.
| Kind | Trigger | Recipients | Default |
|---|---|---|---|
service_enrollment_created |
you approve a service certificate request and a service code is created | every holder of the service account, or the notification address | on |
service_enrollment_redeemed |
one of the codes is redeemed for a certificate, including failed attempts | every holder, or the notification address | on |
service_enrollment_expiring |
a code is close to expiring: weekly inside the reminder window, then daily over the final week | every holder, or the notification address | on |
service_enrollment_expired_attempt |
an expired code is presented for redemption | every holder, or the notification address | on |
user_certificate_issued |
an interactive SSH certificate is signed for you | the person the certificate is for | off |
pam_certificate_issued |
a certificate is signed for a local sudo or su on your behalf |
the person the certificate is for | off |
console_certificate_issued |
a certificate is signed for a console login you approved with a typed code | the person who approved | off |
admin_auto_disabled |
the directory sync disables an account whose most recent login held the admin role | the admin whose account was disabled | always sent |
nss_key_expiring |
an NSS fleet key is close to expiring: weekly inside the reminder window, then daily over the final week | the key’s notification address | always sent |
nss_key_rotation_cleanup |
after mail.rotation_quiet_period, daily while any host still presents a rotated key’s previous secret and its grace lasts |
the key’s notification address | always sent |
nss_key_expired_attempt |
a host presents an expired fleet key, at most once per mail.expired_attempt_window |
the key’s notification address | always sent |
nss_key_revoked_attempt |
a host presents a revoked fleet key, at most once per mail.expired_attempt_window |
the key’s notification address | always sent |
nss_key_rotated |
a fleet key is rotated | the key’s notification address | always sent |
nss_key_revoked |
a fleet key is revoked | the key’s notification address | always sent |
nss_id_change_pending |
the directory gives stored NSS uids or gids a different value: one message per sync run or per batch of reads, listing what is new | every admin, as of their last login | on |
nss_id_change_reminder |
every Monday at 09:00 server time while any uid or gid change is still waiting | every admin, as of their last login | on |
Service code created
Section titled “Service code created”service_enrollment_created
Sent when you approve a service certificate request and a service code is created for it.
Default: on.
Templates:
service_enrollment_created.subject.tmplservice_enrollment_created.txt.tmplservice_enrollment_created.html.tmpl
| Field | Type | Description |
|---|---|---|
.ServiceAccount |
string |
The service account the enrollment was approved for. It is the sole principal of every certificate the code produces. |
.RequestID |
string |
The certificate request this enrollment came from. |
.EnrollmentID |
string |
The enrollment record’s own identifier, as shown in the retrieval log. |
.KeyID |
string |
The SSH certificate key ID fixed at approval time. |
.Principals |
[]string |
The certificate principals fixed at approval time. |
.PublicKeyFingerprint |
string |
SHA256 fingerprint of the enrolled public key. The code only ever produces certificates for this key. |
.PublicKeyType |
string |
SSH algorithm of the enrolled public key, e.g. ssh-ed25519. |
.Extensions |
[]string |
SSH certificate extensions granted, after narrowing against server config. |
.ForceCommand |
string |
The force-command critical option, or empty if none was granted. |
.SourceAddresses |
[]string |
The source-address critical option, or empty if unrestricted. |
.NoTouchRequired |
bool |
Whether the no-touch-required extension was granted (hardware-backed sk- keys only). |
.RequestSourceIP |
string |
The address the enrollment request was submitted from. |
.ApprovedAt |
time.Time |
When the request was approved and the code minted. |
.ApprovedByUsername |
string |
The username of the identity that approved the request. |
.ApprovedByName |
string |
The approver’s human-readable name, or empty if none was captured. |
.ApprovedByEmail |
string |
The approver’s email address, or empty if none was recorded. |
.ApproverSourceIP |
string |
The address the approval was made from, observed by the server. A different question from RequestSourceIP, which is the requester’s. |
.CodeExpiresAt |
time.Time |
When the enrollment code stops being redeemable. Re-enroll before this to keep an unattended job running. |
.CertificateLifetime |
time.Duration |
How long each certificate redeemed from this code is valid for, measured from each redemption. |
.ServerURL |
string |
The server’s public origin, for links back to the request. |
.Advanced |
bool |
True when the recipient chose the advanced email detail level, false for simplified. Set per recipient at delivery; a template that ignores it sends the same message at both levels. |
Service code redeemed
Section titled “Service code redeemed”service_enrollment_redeemed
Sent every time one of your service codes is redeemed for a certificate, including failed attempts.
Default: on.
Templates:
service_enrollment_redeemed.subject.tmplservice_enrollment_redeemed.txt.tmplservice_enrollment_redeemed.html.tmpl
| Field | Type | Description |
|---|---|---|
.ServiceAccount |
string |
The service account the redeemed certificate is for. |
.RequestID |
string |
The certificate request the enrollment came from, or empty for an enrollment with no linked request. |
.EnrollmentID |
string |
The enrollment whose code was redeemed. |
.RetrievalID |
string |
This redemption’s own identifier, matching the row in the retrieval log. |
.SourceIP |
string |
The address the redemption was made from. |
.RetrievedAt |
time.Time |
When the code was redeemed. |
.CertificateSerial |
uint64 |
Serial of the certificate issued for this redemption. |
.CertificateExpiresAt |
time.Time |
When the issued certificate stops being valid. |
.KeyID |
string |
The SSH certificate key ID carried by the issued certificate. |
.Principals |
[]string |
The issued certificate’s principals. |
.Succeeded |
bool |
False when the code was valid but signing failed; the failure detail is in the server log. |
.FirstRedemption |
bool |
True when this was the first time the code was redeemed. |
.CodeExpiresAt |
time.Time |
When the code itself stops being redeemable. |
.ServerURL |
string |
The server’s public origin, for links back to the retrieval log. |
.Advanced |
bool |
True when the recipient chose the advanced email detail level, false for simplified. Set per recipient at delivery; a template that ignores it sends the same message at both levels. |
Service code expiring
Section titled “Service code expiring”service_enrollment_expiring
Sent while one of your service codes is close to expiring, so an unattended job can be enrolled again before it starts failing: weekly inside the reminder window, then daily over the final week.
Default: on.
Templates:
service_enrollment_expiring.subject.tmplservice_enrollment_expiring.txt.tmplservice_enrollment_expiring.html.tmpl
| Field | Type | Description |
|---|---|---|
.ServiceAccount |
string |
The service account the expiring enrollment belongs to. |
.RequestID |
string |
The certificate request the enrollment came from, or empty for an enrollment with no linked request. |
.EnrollmentID |
string |
The enrollment about to expire. |
.KeyID |
string |
The SSH certificate key ID fixed at approval time. |
.Principals |
[]string |
The certificate principals fixed at approval time. |
.PublicKeyFingerprint |
string |
SHA256 fingerprint of the enrolled public key. |
.PublicKeyType |
string |
SSH algorithm of the enrolled public key, e.g. ssh-ed25519. |
.FirstRedeemedAt |
time.Time |
When the code was first redeemed, or the zero time if it never was. A code never redeemed is usually a job that was never finished. |
.CodeExpiresAt |
time.Time |
When the code stops being redeemable. Re-enroll before this. |
.Daily |
bool |
True once the code is inside its final week and reminders come daily; false while they are still weekly. |
.ServerURL |
string |
The server’s public origin, for links back to the enrollment. |
.Advanced |
bool |
True when the recipient chose the advanced email detail level, false for simplified. Set per recipient at delivery; a template that ignores it sends the same message at both levels. |
Expired service code used
Section titled “Expired service code used”service_enrollment_expired_attempt
Sent when an expired service code is presented for redemption: either a job is still trying to use it, or someone is replaying a credential that should no longer exist.
Default: on.
Templates:
service_enrollment_expired_attempt.subject.tmplservice_enrollment_expired_attempt.txt.tmplservice_enrollment_expired_attempt.html.tmpl
| Field | Type | Description |
|---|---|---|
.ServiceAccount |
string |
The service account the expired enrollment belongs to. |
.RequestID |
string |
The certificate request the enrollment came from, or empty for an enrollment with no linked request. |
.EnrollmentID |
string |
The enrollment whose expired code was presented. |
.KeyID |
string |
The SSH certificate key ID fixed at approval time. |
.Principals |
[]string |
The certificate principals fixed at approval time. |
.PublicKeyFingerprint |
string |
SHA256 fingerprint of the enrolled public key. |
.PublicKeyType |
string |
SSH algorithm of the enrolled public key, e.g. ssh-ed25519. |
.SourceIP |
string |
The address the attempt came from. |
.AttemptedAt |
time.Time |
When the expired code was presented. |
.CodeExpiredAt |
time.Time |
When the code stopped being redeemable. |
.ServerURL |
string |
The server’s public origin, for links back to the enrollment. |
.Advanced |
bool |
True when the recipient chose the advanced email detail level, false for simplified. Set per recipient at delivery; a template that ignores it sends the same message at both levels. |
User certificate issued
Section titled “User certificate issued”user_certificate_issued
Sent every time an interactive SSH certificate is signed for you. Off by default: this is one message per login, for people who want to see every one.
Default: off.
Templates:
user_certificate_issued.subject.tmpluser_certificate_issued.txt.tmpluser_certificate_issued.html.tmpl
| Field | Type | Description |
|---|---|---|
.CertificateType |
string |
The certificate type: “user”, “pam” or “console”. |
.RequestID |
string |
The certificate request this certificate was issued for. |
.CertificateID |
string |
The certificate’s own record, for a link to /certs/ |
.KeyID |
string |
The SSH certificate key ID. |
.Principals |
[]string |
The accounts this certificate may log in as. |
.Serial |
uint64 |
The certificate serial, matching the entry in your certificate history. |
.PublicKeyFingerprint |
string |
SHA256 fingerprint of the key the certificate was issued for. |
.LocalUsername |
string |
The local account the client reported, or empty if it reported none. Client-reported, so not evidence. |
.LocalHostname |
string |
The machine the client reported, or empty if it reported none. Client-reported, so not evidence. |
.RequestingUser |
string |
Who invoked the request, when that differs from the account it ran as: PAM_RUSER for a module, SUDO_USER for the client. Client-reported, so not evidence. |
.Process |
string |
The command line that asked, e.g. “sudo systemctl restart nginx” or “ssoossh ssh login”. Client-reported, so not evidence. |
.TTY |
string |
The terminal the request came from, or empty if it had none. Client-reported, so not evidence. |
.RemoteHost |
string |
The peer address the requester reported, non-empty only when it was itself reached over the network. Client-reported, so not evidence. |
.PAMService |
string |
The PAM service that asked (sudo, su, sshd, login). Empty on a user certificate, which has none. |
.MachineID |
string |
The requesting machine’s stable installation id, which survives a rename. Client-reported, so not evidence. |
.OS |
string |
The platform as the requesting machine describes itself. Client-reported, so not evidence. |
.Client |
string |
The implementation and version that sent the request, e.g. “ssoossh/1.2.3” or “pam_ssoossh-c/0.3.0”. |
.ClientMode |
string |
The PAM module’s configured mode argument (auto, sudo, console). Empty for the client, which has none. |
.CallerUID |
*int64 |
The requesting process’s user id, or absent if it reported none. A pointer because uid 0 is a value. |
.CallerGID |
*int64 |
The requesting process’s group id, or absent if it reported none. |
.CallerPID |
*int64 |
The requesting process’s id, for joining against the machine’s own logs. |
.CallerPPID |
*int64 |
The requesting process’s parent id. |
.SourceIP |
string |
The address the request was made from. Observed by the server, unlike everything the client reported. |
.IssuedAt |
time.Time |
When the certificate becomes valid. |
.ExpiresAt |
time.Time |
When the certificate stops being valid. |
.ApprovedByUsername |
string |
The account that approved the request, or empty if no decision record survives. |
.ApprovedByName |
string |
The approver’s human-readable name, or empty if none was captured. |
.ApprovedByEmail |
string |
The approver’s email address, or empty if none was recorded. |
.ApproverSourceIP |
string |
The address the approval was made from, observed by the server. On the unhappy path this is the fact that says whose session was used. |
.ApproverUserAgent |
string |
The browser that approved, as it identified itself. |
.ApprovedAt |
time.Time |
When the approval happened. Zero if no decision record survives. |
.Extensions |
[]string |
SSH certificate extensions granted, after narrowing against server config. |
.ForceCommand |
string |
The force-command critical option, or empty if none was granted. |
.SourceAddresses |
[]string |
The source-address critical option, or empty if unrestricted. |
.NoTouchRequired |
bool |
Whether the certificate waives the hardware-key touch, which makes it a weaker credential than one that does not. |
.ServerURL |
string |
The server’s public origin, for links back to the certificate. |
.Advanced |
bool |
True when the recipient chose the advanced email detail level, false for simplified. Set per recipient at delivery; a template that ignores it sends the same message at both levels. |
PAM certificate issued
Section titled “PAM certificate issued”pam_certificate_issued
Sent every time a certificate is signed for a local sudo or su on your behalf. Off by default: this is one message per sudo.
Default: off.
Templates:
pam_certificate_issued.subject.tmplpam_certificate_issued.txt.tmplpam_certificate_issued.html.tmpl
| Field | Type | Description |
|---|---|---|
.CertificateType |
string |
The certificate type: “user”, “pam” or “console”. |
.RequestID |
string |
The certificate request this certificate was issued for. |
.CertificateID |
string |
The certificate’s own record, for a link to /certs/ |
.KeyID |
string |
The SSH certificate key ID. |
.Principals |
[]string |
The accounts this certificate may log in as. |
.Serial |
uint64 |
The certificate serial, matching the entry in your certificate history. |
.PublicKeyFingerprint |
string |
SHA256 fingerprint of the key the certificate was issued for. |
.LocalUsername |
string |
The local account the client reported, or empty if it reported none. Client-reported, so not evidence. |
.LocalHostname |
string |
The machine the client reported, or empty if it reported none. Client-reported, so not evidence. |
.RequestingUser |
string |
Who invoked the request, when that differs from the account it ran as: PAM_RUSER for a module, SUDO_USER for the client. Client-reported, so not evidence. |
.Process |
string |
The command line that asked, e.g. “sudo systemctl restart nginx” or “ssoossh ssh login”. Client-reported, so not evidence. |
.TTY |
string |
The terminal the request came from, or empty if it had none. Client-reported, so not evidence. |
.RemoteHost |
string |
The peer address the requester reported, non-empty only when it was itself reached over the network. Client-reported, so not evidence. |
.PAMService |
string |
The PAM service that asked (sudo, su, sshd, login). Empty on a user certificate, which has none. |
.MachineID |
string |
The requesting machine’s stable installation id, which survives a rename. Client-reported, so not evidence. |
.OS |
string |
The platform as the requesting machine describes itself. Client-reported, so not evidence. |
.Client |
string |
The implementation and version that sent the request, e.g. “ssoossh/1.2.3” or “pam_ssoossh-c/0.3.0”. |
.ClientMode |
string |
The PAM module’s configured mode argument (auto, sudo, console). Empty for the client, which has none. |
.CallerUID |
*int64 |
The requesting process’s user id, or absent if it reported none. A pointer because uid 0 is a value. |
.CallerGID |
*int64 |
The requesting process’s group id, or absent if it reported none. |
.CallerPID |
*int64 |
The requesting process’s id, for joining against the machine’s own logs. |
.CallerPPID |
*int64 |
The requesting process’s parent id. |
.SourceIP |
string |
The address the request was made from. Observed by the server, unlike everything the client reported. |
.IssuedAt |
time.Time |
When the certificate becomes valid. |
.ExpiresAt |
time.Time |
When the certificate stops being valid. |
.ApprovedByUsername |
string |
The account that approved the request, or empty if no decision record survives. |
.ApprovedByName |
string |
The approver’s human-readable name, or empty if none was captured. |
.ApprovedByEmail |
string |
The approver’s email address, or empty if none was recorded. |
.ApproverSourceIP |
string |
The address the approval was made from, observed by the server. On the unhappy path this is the fact that says whose session was used. |
.ApproverUserAgent |
string |
The browser that approved, as it identified itself. |
.ApprovedAt |
time.Time |
When the approval happened. Zero if no decision record survives. |
.Extensions |
[]string |
SSH certificate extensions granted, after narrowing against server config. |
.ForceCommand |
string |
The force-command critical option, or empty if none was granted. |
.SourceAddresses |
[]string |
The source-address critical option, or empty if unrestricted. |
.NoTouchRequired |
bool |
Whether the certificate waives the hardware-key touch, which makes it a weaker credential than one that does not. |
.ServerURL |
string |
The server’s public origin, for links back to the certificate. |
.Advanced |
bool |
True when the recipient chose the advanced email detail level, false for simplified. Set per recipient at delivery; a template that ignores it sends the same message at both levels. |
Console certificate issued
Section titled “Console certificate issued”console_certificate_issued
Sent every time a certificate is signed for a console login you approved with a typed code. Off by default: this is one message per login.
Default: off.
Templates:
console_certificate_issued.subject.tmplconsole_certificate_issued.txt.tmplconsole_certificate_issued.html.tmpl
| Field | Type | Description |
|---|---|---|
.CertificateType |
string |
The certificate type: “user”, “pam” or “console”. |
.RequestID |
string |
The certificate request this certificate was issued for. |
.CertificateID |
string |
The certificate’s own record, for a link to /certs/ |
.KeyID |
string |
The SSH certificate key ID. |
.Principals |
[]string |
The accounts this certificate may log in as. |
.Serial |
uint64 |
The certificate serial, matching the entry in your certificate history. |
.PublicKeyFingerprint |
string |
SHA256 fingerprint of the key the certificate was issued for. |
.LocalUsername |
string |
The local account the client reported, or empty if it reported none. Client-reported, so not evidence. |
.LocalHostname |
string |
The machine the client reported, or empty if it reported none. Client-reported, so not evidence. |
.RequestingUser |
string |
Who invoked the request, when that differs from the account it ran as: PAM_RUSER for a module, SUDO_USER for the client. Client-reported, so not evidence. |
.Process |
string |
The command line that asked, e.g. “sudo systemctl restart nginx” or “ssoossh ssh login”. Client-reported, so not evidence. |
.TTY |
string |
The terminal the request came from, or empty if it had none. Client-reported, so not evidence. |
.RemoteHost |
string |
The peer address the requester reported, non-empty only when it was itself reached over the network. Client-reported, so not evidence. |
.PAMService |
string |
The PAM service that asked (sudo, su, sshd, login). Empty on a user certificate, which has none. |
.MachineID |
string |
The requesting machine’s stable installation id, which survives a rename. Client-reported, so not evidence. |
.OS |
string |
The platform as the requesting machine describes itself. Client-reported, so not evidence. |
.Client |
string |
The implementation and version that sent the request, e.g. “ssoossh/1.2.3” or “pam_ssoossh-c/0.3.0”. |
.ClientMode |
string |
The PAM module’s configured mode argument (auto, sudo, console). Empty for the client, which has none. |
.CallerUID |
*int64 |
The requesting process’s user id, or absent if it reported none. A pointer because uid 0 is a value. |
.CallerGID |
*int64 |
The requesting process’s group id, or absent if it reported none. |
.CallerPID |
*int64 |
The requesting process’s id, for joining against the machine’s own logs. |
.CallerPPID |
*int64 |
The requesting process’s parent id. |
.SourceIP |
string |
The address the request was made from. Observed by the server, unlike everything the client reported. |
.IssuedAt |
time.Time |
When the certificate becomes valid. |
.ExpiresAt |
time.Time |
When the certificate stops being valid. |
.ApprovedByUsername |
string |
The account that approved the request, or empty if no decision record survives. |
.ApprovedByName |
string |
The approver’s human-readable name, or empty if none was captured. |
.ApprovedByEmail |
string |
The approver’s email address, or empty if none was recorded. |
.ApproverSourceIP |
string |
The address the approval was made from, observed by the server. On the unhappy path this is the fact that says whose session was used. |
.ApproverUserAgent |
string |
The browser that approved, as it identified itself. |
.ApprovedAt |
time.Time |
When the approval happened. Zero if no decision record survives. |
.Extensions |
[]string |
SSH certificate extensions granted, after narrowing against server config. |
.ForceCommand |
string |
The force-command critical option, or empty if none was granted. |
.SourceAddresses |
[]string |
The source-address critical option, or empty if unrestricted. |
.NoTouchRequired |
bool |
Whether the certificate waives the hardware-key touch, which makes it a weaker credential than one that does not. |
.ServerURL |
string |
The server’s public origin, for links back to the certificate. |
.Advanced |
bool |
True when the recipient chose the advanced email detail level, false for simplified. Set per recipient at delivery; a template that ignores it sends the same message at both levels. |
Admin account disabled by the directory sync
Section titled “Admin account disabled by the directory sync”admin_auto_disabled
Sent to an admin when the scheduled directory sync disables their account because its directory entry has been missing for longer than ldap.sync.disable_after. Always sent: it cannot be turned off, and it is not sent for a disable a SOC user or an admin made by hand.
Always sent: it is not a preference and cannot be turned off.
Templates:
admin_auto_disabled.subject.tmpladmin_auto_disabled.txt.tmpladmin_auto_disabled.html.tmpl
| Field | Type | Description |
|---|---|---|
.Username |
string |
The username of the account that was disabled. |
.DisabledAt |
time.Time |
When the sync disabled the account. |
.MissingSince |
time.Time |
When the sync first searched successfully and found no directory entry for the account. |
.MissingFor |
time.Duration |
How long the entry had been missing when the account was disabled. |
.DisableAfter |
time.Duration |
The configured ldap.sync.disable_after: how long an entry may stay missing before the sync disables the account. |
.DirectoryDN |
string |
The last DN the account’s entry was found at, or empty if none was recorded. |
.BaseDN |
string |
The configured ldap.base_dn the sync searches under. |
.Reenable |
bool |
Whether ldap.sync.reenable is on, in which case the sync clears this disable by itself once the entry is found again. |
.SupportEmail |
string |
Who to ask about this deployment (branding.support_email), or empty if none is configured. |
.ServerURL |
string |
The server’s public origin, naming the deployment the account belongs to. |
.Advanced |
bool |
True when the recipient chose the advanced email detail level, false for simplified. Set per recipient at delivery; a template that ignores it sends the same message at both levels. |
OIDC UserInfo calls paused
Section titled “OIDC UserInfo calls paused”userinfo_paused
Sent to every admin when the server stops calling the identity provider’s UserInfo endpoint after 10 failed calls in a row. Logins carry on with the ID token’s claims alone, and one login every 15 minutes retries the call. Always sent: it cannot be turned off.
Always sent: it is not a preference and cannot be turned off.
Templates:
userinfo_paused.subject.tmpluserinfo_paused.txt.tmpluserinfo_paused.html.tmpl
| Field | Type | Description |
|---|---|---|
.Endpoint |
string |
The UserInfo endpoint the provider’s discovery document names. |
.PausedAt |
time.Time |
When the server paused the calls. |
.ConsecutiveFailures |
int |
How many calls in a row had failed, across every instance. |
.LastError |
string |
The last failed call’s error, with the access token removed. |
.NextProbeAt |
time.Time |
When the first retry may run: the first login after this time makes the call. |
.ServerURL |
string |
The server’s public origin, naming the deployment. |
.Advanced |
bool |
True when the recipient chose the advanced email detail level, false for simplified. Set per recipient at delivery; a template that ignores it sends the same message at both levels. |
OIDC UserInfo calls resumed
Section titled “OIDC UserInfo calls resumed”userinfo_resumed
Sent to every admin when a retry of the identity provider’s UserInfo endpoint succeeds after a pause, and logins read its claims again. Always sent: it cannot be turned off.
Always sent: it is not a preference and cannot be turned off.
Templates:
userinfo_resumed.subject.tmpluserinfo_resumed.txt.tmpluserinfo_resumed.html.tmpl
| Field | Type | Description |
|---|---|---|
.Endpoint |
string |
The UserInfo endpoint the provider’s discovery document names. |
.PausedAt |
time.Time |
When the server had paused the calls. |
.ResumedAt |
time.Time |
When the retry succeeded and the calls resumed. |
.ServerURL |
string |
The server’s public origin, naming the deployment. |
.Advanced |
bool |
True when the recipient chose the advanced email detail level, false for simplified. Set per recipient at delivery; a template that ignores it sends the same message at both levels. |
NSS fleet key expiring
Section titled “NSS fleet key expiring”nss_key_expiring
Sent to the key’s notification address (nss_keys.notification_email, which defaults to the creator’s address) while a fleet key nears expiry: weekly inside mail.expiry_reminder_lead, then daily over the final week. Always sent, and not a preference: it cannot be turned off.
Always sent: it is not a preference and cannot be turned off.
Templates:
nss_key_expiring.subject.tmplnss_key_expiring.txt.tmplnss_key_expiring.html.tmpl
| Field | Type | Description |
|---|---|---|
.KeyID |
string |
The fleet key’s id, “nss_” followed by 8 characters. |
.KeyName |
string |
The fleet key’s name. |
.Description |
string |
The key’s description, or empty if none was set. |
.GroupName |
string |
The group whose members manage the key, or empty for none. |
.ExpiresAt |
time.Time |
When the key stops being accepted. Extend it, or create a replacement, before this. |
.Daily |
bool |
True once the key is inside its final week and reminders come daily; false while they are still weekly. |
.ServerURL |
string |
The server’s public origin, naming the deployment the key belongs to. |
.Advanced |
bool |
True when the recipient chose the advanced email detail level, false for simplified. Set per recipient at delivery; a template that ignores it sends the same message at both levels. |
NSS fleet key rotation cleanup
Section titled “NSS fleet key rotation cleanup”nss_key_rotation_cleanup
Sent to the key’s notification address (nss_keys.notification_email, which defaults to the creator’s address) after a rotation, once mail.rotation_quiet_period has passed, and daily while any host still presents the previous secret, until the grace period ends. It lists the addresses that have not switched. Always sent, and not a preference: it cannot be turned off.
Always sent: it is not a preference and cannot be turned off.
Templates:
nss_key_rotation_cleanup.subject.tmplnss_key_rotation_cleanup.txt.tmplnss_key_rotation_cleanup.html.tmpl
| Field | Type | Description |
|---|---|---|
.KeyID |
string |
The fleet key’s id, “nss_” followed by 8 characters. |
.KeyName |
string |
The fleet key’s name. |
.Generation |
int |
The key’s current generation, the one hosts should present. |
.RotatedAt |
time.Time |
When the key was last rotated. |
.PreviousExpiresAt |
time.Time |
When the previous secret stops working. Equal to RotatedAt when no grace period is set. |
.Switched |
int |
Addresses whose last lookup presented the current secret. |
.NotSwitched |
int |
Addresses whose last lookup presented the previous secret. |
.Gone |
int |
Addresses seen before the rotation and not since. |
.NotSwitchedAddresses |
[]string |
Up to 50 addresses that have not switched, most recently seen first. |
.MoreNotSwitched |
int |
How many more not-switched addresses there are beyond the listed ones, or zero. |
.ServerURL |
string |
The server’s public origin, naming the deployment the key belongs to. |
.Advanced |
bool |
True when the recipient chose the advanced email detail level, false for simplified. Set per recipient at delivery; a template that ignores it sends the same message at both levels. |
Expired NSS fleet key used
Section titled “Expired NSS fleet key used”nss_key_expired_attempt
Sent to the key’s notification address (nss_keys.notification_email, which defaults to the creator’s address) when a host presents an expired fleet key, at most once per mail.expired_attempt_window. The attempt is refused. Always sent, and not a preference: it cannot be turned off.
Always sent: it is not a preference and cannot be turned off.
Templates:
nss_key_expired_attempt.subject.tmplnss_key_expired_attempt.txt.tmplnss_key_expired_attempt.html.tmpl
| Field | Type | Description |
|---|---|---|
.KeyID |
string |
The fleet key’s id, “nss_” followed by 8 characters. |
.KeyName |
string |
The fleet key’s name. |
.SourceIP |
string |
The address the attempt came from, as the server observed it. |
.AttemptedAt |
time.Time |
When the expired key was presented. |
.ExpiredAt |
time.Time |
When the key stopped being accepted. |
.ServerURL |
string |
The server’s public origin, naming the deployment the key belongs to. |
.Advanced |
bool |
True when the recipient chose the advanced email detail level, false for simplified. Set per recipient at delivery; a template that ignores it sends the same message at both levels. |
Revoked NSS fleet key used
Section titled “Revoked NSS fleet key used”nss_key_revoked_attempt
Sent to the key’s notification address (nss_keys.notification_email, which defaults to the creator’s address) when a host presents a revoked fleet key, at most once per mail.expired_attempt_window. The attempt is refused. Always sent, and not a preference: it cannot be turned off.
Always sent: it is not a preference and cannot be turned off.
Templates:
nss_key_revoked_attempt.subject.tmplnss_key_revoked_attempt.txt.tmplnss_key_revoked_attempt.html.tmpl
| Field | Type | Description |
|---|---|---|
.KeyID |
string |
The fleet key’s id, “nss_” followed by 8 characters. |
.KeyName |
string |
The fleet key’s name. |
.SourceIP |
string |
The address the attempt came from, as the server observed it. |
.AttemptedAt |
time.Time |
When the revoked key was presented. |
.RevokedAt |
time.Time |
When the key was revoked. |
.ServerURL |
string |
The server’s public origin, naming the deployment the key belongs to. |
.Advanced |
bool |
True when the recipient chose the advanced email detail level, false for simplified. Set per recipient at delivery; a template that ignores it sends the same message at both levels. |
NSS fleet key rotated
Section titled “NSS fleet key rotated”nss_key_rotated
Sent to the key’s notification address (nss_keys.notification_email, which defaults to the creator’s address) at once when a fleet key is rotated. Always sent, and not a preference: it cannot be turned off.
Always sent: it is not a preference and cannot be turned off.
Templates:
nss_key_rotated.subject.tmplnss_key_rotated.txt.tmplnss_key_rotated.html.tmpl
| Field | Type | Description |
|---|---|---|
.KeyID |
string |
The fleet key’s id, “nss_” followed by 8 characters. |
.KeyName |
string |
The fleet key’s name. |
.Generation |
int |
The new current generation. |
.RotatedAt |
time.Time |
When the key was rotated. |
.RotatedByUsername |
string |
The account that rotated the key. |
.RotatedByName |
string |
That account’s human-readable name, or empty if none was captured. |
.PreviousExpiresAt |
time.Time |
When the previous secret stops working. Equal to RotatedAt when no grace period is set or the key had expired. |
.ExpiresAt |
time.Time |
The key’s expiry as the rotation renewed it: its own lifetime again from the rotation, at most nss.key_max_lifetime. Zero for a key that never expires. |
.ServerURL |
string |
The server’s public origin, naming the deployment the key belongs to. |
.Advanced |
bool |
True when the recipient chose the advanced email detail level, false for simplified. Set per recipient at delivery; a template that ignores it sends the same message at both levels. |
NSS fleet key revoked
Section titled “NSS fleet key revoked”nss_key_revoked
Sent to the key’s notification address (nss_keys.notification_email, which defaults to the creator’s address) at once when a fleet key is revoked. Always sent, and not a preference: it cannot be turned off.
Always sent: it is not a preference and cannot be turned off.
Templates:
nss_key_revoked.subject.tmplnss_key_revoked.txt.tmplnss_key_revoked.html.tmpl
| Field | Type | Description |
|---|---|---|
.KeyID |
string |
The fleet key’s id, “nss_” followed by 8 characters. |
.KeyName |
string |
The fleet key’s name. |
.RevokedAt |
time.Time |
When the key was revoked. |
.RevokedByUsername |
string |
The account that revoked the key. |
.RevokedByName |
string |
That account’s human-readable name, or empty if none was captured. |
.ServerURL |
string |
The server’s public origin, naming the deployment the key belongs to. |
.Advanced |
bool |
True when the recipient chose the advanced email detail level, false for simplified. Set per recipient at delivery; a template that ignores it sends the same message at both levels. |
Directory id change waiting for approval
Section titled “Directory id change waiting for approval”nss_id_change_pending
Sent to every admin when the directory gives a stored NSS account or group a different uid or gid. Hosts keep the stored number until an admin accepts the change. One message per sync run, or per batch of on-demand and login reads, listing only the changes that are new.
Default: on.
Templates:
nss_id_change_pending.subject.tmplnss_id_change_pending.txt.tmplnss_id_change_pending.html.tmpl
| Field | Type | Description |
|---|---|---|
.Changes |
[]notify.NSSIDChange |
The listed changes, oldest first, at most 50. Each has Entry (“account” or “group”), Attribute (“uid” or “gid”), Name, DN, DirectoryID (the ldap.id_attribute value, or empty), Stored (the number hosts are served), Pending (the directory’s new number), PendingSince (when the server first saw it) and Waiting (how long it has waited, as a time.Duration). An account whose uid and gid both changed is two changes. |
.Total |
int |
How many changes the message is about, the listed ones included. |
.More |
int |
How many changes there are beyond the listed ones, or zero. |
.DirectoryURL |
string |
The pending changes on the admin directory page, where an admin accepts them: ServerURL followed by /admin/directory#pending-id-changes. |
.ServerURL |
string |
The server’s public origin, naming the deployment the directory belongs to. |
.Advanced |
bool |
True when the recipient chose the advanced email detail level, false for simplified. Set per recipient at delivery; a template that ignores it sends the same message at both levels. |
Weekly reminder of waiting directory id changes
Section titled “Weekly reminder of waiting directory id changes”nss_id_change_reminder
Sent to every admin on Monday at 09:00 server time while any uid or gid change is still waiting, listing all of them with how long each has waited.
Default: on.
Templates:
nss_id_change_reminder.subject.tmplnss_id_change_reminder.txt.tmplnss_id_change_reminder.html.tmpl
| Field | Type | Description |
|---|---|---|
.Changes |
[]notify.NSSIDChange |
The listed changes, oldest first, at most 50. Each has Entry (“account” or “group”), Attribute (“uid” or “gid”), Name, DN, DirectoryID (the ldap.id_attribute value, or empty), Stored (the number hosts are served), Pending (the directory’s new number), PendingSince (when the server first saw it) and Waiting (how long it has waited, as a time.Duration). An account whose uid and gid both changed is two changes. |
.Total |
int |
How many changes the message is about, the listed ones included. |
.More |
int |
How many changes there are beyond the listed ones, or zero. |
.DirectoryURL |
string |
The pending changes on the admin directory page, where an admin accepts them: ServerURL followed by /admin/directory#pending-id-changes. |
.ServerURL |
string |
The server’s public origin, naming the deployment the directory belongs to. |
.Advanced |
bool |
True when the recipient chose the advanced email detail level, false for simplified. Set per recipient at delivery; a template that ignores it sends the same message at both levels. |
Overriding a template
Section titled “Overriding a template”Every message ships as a built-in template compiled into the binary. Point
mail.template_dir at a
directory to replace any of them:
mail: template_dir: "/etc/ssoossh/mail-templates"A file there replaces the built-in template of the same name. Anything absent falls back to the built-in one, so overriding a single message does not mean vendoring the whole set – and a message added in a later release keeps working without you touching the directory.
Files are named <kind>.<part>.tmpl, where <part> is one of:
| Part | Engine | Purpose |
|---|---|---|
subject |
text/template |
The subject line. Rendered to one line: any line break is folded to a space, so nothing interpolated into it can inject headers. mail.subject_prefix is prepended afterwards |
txt |
text/template |
The plain-text body |
html |
html/template |
The HTML body, sent as an alternative. Interpolated values are HTML-escaped |
Both bodies are always sent. A text-only reader gets the whole message rather than a notice telling them to view it elsewhere.
The field tables above list what each template may reference. Everything in a notification’s payload appears there – a test fails when the tables and the payload structs disagree in either direction, so a field that exists is a field you can render, and one you can render is one that is documented.
Optional fields are the ones to guard. A user certificate has no .PAMService,
a local terminal has no .RemoteHost, a Windows client reports no .CallerUID,
and a certificate whose decision record has gone has no .ApprovedByUsername.
Wrap each in an {{ if }} rather than assuming it is there:
{{ if .Process }} Command {{ .Process }}{{ end }}The .Caller* fields are pointers, so {{ if .CallerUID }} is false when the
platform reported none and true when it reported zero – and {{ .CallerUID }}
prints the number, not an address. Times are time.Time, so
{{ if not .ApprovedAt.IsZero }} is how you tell “not recorded” from a real
timestamp.
In addition, these functions are available in every template:
| Function | Example | Renders |
|---|---|---|
datetime |
{{ datetime .ApprovedAt }} |
2026-08-24 15:04 CEST, or not set for a zero time |
date |
{{ date .CodeExpiresAt }} |
2026-11-22, or not set |
approx |
{{ approx .CertificateLifetime }} |
8 hours, 90 days – the largest unit that still says something, rounded to the nearest one |
until |
{{ until .CodeExpiresAt }} |
89 days, or already elapsed |
remaining |
{{ remaining .ExpiresAt }} |
~8h left, or expired – one rounded unit, in the compact form the web UI prints beside a validity period |
join |
{{ join .Principals ", " }} |
deploy-bot, deploy-bot-2 |
Timestamps render in the server’s local zone, with the zone named.
Overrides are checked at startup
Section titled “Overrides are checked at startup”Every override is parsed and executed against an empty payload, at both
detail levels, when the server starts. A syntax error, or a reference to a
field that does not exist, stops the server rather than producing mail that
silently stops arriving – including one inside an {{ if .Advanced }} block,
which a single run would skip. An override that never reads .Advanced sends
the same message at both levels. A
file in the directory whose name matches no notification is also a startup
error – a misspelled override would otherwise appear to do nothing. Files
that are not *.tmpl are ignored, so a README or an editor backup beside your
overrides is fine.
The built-in templates are the best starting point for an override: copy one out, edit it, and drop it in the override directory under the same name. Reference copies ship with the server, so you do not need a source tree:
| Install | Where the reference copies are |
|---|---|
.deb / .rpm / .apk |
/usr/share/ssoossh/mail-templates/ |
| Container image | /usr/share/ssoossh/mail-templates/ |
| Release archive | mail-templates/ inside the tarball |
| Source tree | server/resources/mail/ |
Adding a notification kind
Section titled “Adding a notification kind”That is a code change rather than a configuration one: a Kind constant, a
payload struct, a registry Definition, and three templates, after which the
preferences page, the reference table, and delivery all follow from the
registry. The steps are documented for developers in
Adding a notification kind
in the repository.