Releases

Releases

Every tagged release on GitHub appears here automatically. We use semantic date-stamped tags (`v260120` = 2026-01-20) for predictable cadence and so you can correlate releases with the changelog at a glance.

Hermes SEG v260814

v260814 August 14, 2026

Hermes SEG v260814

A fresh-install release. Two defects that could strand a new gateway partway through
its first install are fixed, plus one dashboard failure that affects gateways already
running.

Neither install defect is a v260807 regression. Both have been present since the
Docker edition's first commit and were found by running the fresh-install gate that
v260807 shipped without.

Read this first

If your gateway is already running, one thing here affects you

The dashboard fix. Everything else in this release is installer work that only
executes while a gateway is being built for the first time, and there is no schema
change at all. Upgrading is quick and low risk.

The admin console could be taken down by a missing disk probe

The dashboard draws five storage rings, one per tier. Each ran a probe script inside
a cftry whose cfcatch rendered an error page and aborted. So if a probe script was
absent, or its path was not mounted, the entire dashboard died with a
page-specific error rather than losing a single ring.

This was reachable on any gateway installed before the archive tier was added in
#260, because those installs have no disk_space_usage_archive.sh. Observed in the
field: index.cfm completely unreachable, with the rest of the console fine.

The catch now leaves the ring's default in place and returns. Every other panel
renders and the affected ring reads zero.

A related defect was fixed in v260807: cfexecute only creates its output variable
when the command actually produces output, so a probe that returned nothing left the
variable undefined and killed the page the same way. Both halves are now closed.

Nextcloud got one install attempt and could never recover from failing it

The nextcloud:apache entrypoint runs occ maintenance:install exactly once, on
first boot. If MariaDB has not finished creating the Nextcloud database user by the
time that runs, the attempt fails and leaves a partial config.php behind: it
carries dbname and dbhost, but no dbuser, no dbpassword, and no installed
key.

On every later boot the entrypoint reads that file, concludes Nextcloud is already
configured, and never tries again. The container sits there up and healthy, and
uninstalled, indefinitely.

This is why it was not a timeout. The installer waited two minutes and then advised
re-running --init-db, but that advice could not work: the re-run reached the
same already-configured short circuit, so the loop never terminated. Widening the
wait would not have helped either, because nothing was still running to wait for.

The installer now drives the install itself when it finds Nextcloud reporting
installed: false. It preserves the partial config.php under a date-stamped name,
confirms the database user can actually open the database, then runs
maintenance:install with the credentials it already generated. The two-minute poll
is kept as the fast path, so a healthy install behaves exactly as before.

Tracked in #313.

A wrong host clock took DNS down and the installer blamed your forwarders

Hermes runs its own Unbound resolver with DNSSEC validation enabled. Validation
happens locally, against the host clock, on every answer Unbound receives.
Forwarding does not hand that job to your upstream resolver: Unbound still checks the
signatures itself.

So when the host clock is wrong, every signature reads as invalid. Because the
failing signature is on the root zone, Unbound cannot establish trust for anything
beneath it, and all name resolution stops, not just signed zones. Unbound then
caches the invalid key, so correcting the clock alone does not restore service. And
because NTP server hostnames no longer resolve, the clock cannot fix itself.

The installer's DNS preflight reported this as a forwarder problem, which is the one
thing it is not. Reverting a virtual machine snapshot is the ordinary way to end up
here.

The preflight now recognises the signature. When DNS fails it checks Unbound's log
for signature before inception, signature expired and
key for validation ... marked as invalid, and separately checks whether the host
clock is behind the commit being installed, which proves the clock is wrong without
needing any network access. If either fires it names the clock, shows the evidence,
and gives the fix including the Unbound restart that the cached state requires.

This is diagnosis only. No new check gates a healthy install, and nothing new
runs unless DNS has already failed. A correct clock that happens not to be managed by
systemd will not be flagged.

Tracked in #314.

The release workflow could not run its own drift check

scripts/check_ofelia_seed_drift.sh was committed non-executable, so the
release-images workflow failed at "Verify generated artifacts" with permission
denied and skipped the retag job. Same class as the pre-push hook in #296: a script
that only ever runs from a fresh checkout, where the git file mode is the only mode
that counts.

No effect on v260807, whose images were promoted by hand from the tested artifacts.

What to do

Run the standard upgrade:

cd /opt/hermes-seg-docker-gl
sudo ./scripts/system_update_docker.sh

There is nothing to do by hand. No schema change, no config template change, and no
page that needs re-saving.

How to confirm it worked

# Build stamp advanced
docker exec hermes_db_server mysql -u root hermes \
  -e "SELECT value FROM system_settings WHERE parameter = 'build_no';"

expect: v260814

Then open the admin console dashboard. All five storage rings should render. A tier
you have not provisioned reads zero instead of taking the page down with it.

Why it happened

The two installer defects are the same story told twice: a first-run path that
nothing exercised
. Both date to the Docker edition's root commit and both survived
every release because upgrade testing never touches them, and until v260807 there was
no fresh-install gate to catch them. git log -S confirms neither was introduced by
a recent change.

The dashboard defect is a different shape. It is the cost of a cfcatch that aborts:
a handler written to make one failure loud made every failure fatal. The fix is to
degrade the ring, not the page.

What changed

| Area | Change | Issue |
| --- | --- | --- |
| Installer | Nextcloud install recovered rather than abandoned | #313 |
| Installer | DNS failure names the clock instead of the forwarders | #314 |
| Dashboard | A failed disk probe loses one ring, not the console | |
| CI | check_ofelia_seed_drift.sh committed executable | |
| Schema | Version stamp only, no changes | |

Known follow-up

  • #311 Distribution lists. One address expanding to many recipients.
  • #312 The Fail2Ban whitelist is not discoverable until after you have locked

yourself out.

  • #306 The CipherMail console still has its own unmanaged administrator account.

Hermes SEG v260807

v260807 August 13, 2026

Hermes SEG v260807

First-run provisioning fixes. Every defect below has been present since the Docker
edition shipped, and each one is invisible on any gateway where an administrator
happened to save the relevant settings page.

Read this first

This upgrade clears your SpamAssassin Bayes database

Hermes has been shipping a pre-trained Bayes corpus in the repository: roughly 470
spam and 540 ham messages trained on unrelated mail between 2020 and 2025. Every
install has been scoring your mail partly against that foreign training.

SpamAssassin merges all learning into a single token store, so anything you trained
locally is inseparable from the shipped data. There is no way to remove one and keep
the other. The upgrade clears the database once, and your gateway relearns from
its own traffic.

You do not need to do anything. Mail filtering continues via the rule set, RBLs,
ClamAV and the malware feeds. Bayes contributes no score until it has learned roughly
200 spam and 200 ham, which you build with Train as Spam and Train as Ham in
Quarantine and Message History.

This applies even if you have trained the gateway yourself. A corpus that contains
the seeded data is not worth keeping at any ratio, and there is no way to separate
the two, so the database is cleared outright rather than preserved in a compromised
state. Restoring a backup of it afterwards would reintroduce the foreign training
permanently: the upgrade records the clearing as done, so nothing would ever remove
it again.

Spam was getting a 5 point discount on every message

SpamAssassin ships three rules querying Validity, the sender-reputation service
formerly known as Return Path. Two of them are allowlists with large negative
scores, meant for the small number of senders who pay Validity to be certified.

Validity refuses queries from unregistered resolvers and answers
127.255.255.255. SpamAssassin matches that as a hit, so both allowlist rules
fire on every message and award it -5. Every Hermes install resolves through
its own recursive Unbound instance, so the query always comes from an
unregistered address and there is no configuration in which a stock install
avoids this.

The practical effect is that your configured thresholds have been behaving five
points higher than they read. On the default policy, a message needed to score 7
to be quarantined rather than 2.

The three rules are now scored 0. They still evaluate and still appear in
X-Spam-Status, so nothing becomes harder to diagnose. If you are registered
with Validity and want them back, set the scores through Score Overrides;
those render after these defaults and SpamAssassin honours the last one.

Expect more mail to be caught after upgrading. That is the intended
correction, but if you had tuned thresholds around the old behaviour, review
them.

Mail clients could not send: SMTP submission was never enabled

master.cf shipped with the submission (587) and smtps (465) listeners commented
out, in every variant in the repository. Docker published those ports and the mailbox
domain page told you to advertise them over SRV, but Postfix was not listening on
either.

On a mailbox or hybrid install this means no user could send mail from Thunderbird,
a phone, or any other client
. Receiving worked, and webmail worked, because
Nextcloud Mail reaches Postfix on port 25 across the Docker network rather than
through submission. That is why the gap survived: the two paths most people test
first were both unaffected.

Both listeners are now enabled, with SASL answered by Dovecot and
reject_sender_login_mismatch so an authenticated user cannot send as somebody else.

> If you enabled submission by hand, the upgrade replaces master.cf with the
> repository version and your edit is lost. The shipped configuration is a superset
> of the usual manual fix, so submission keeps working, but check the file afterwards
> if you customised anything else in it.

Amavis quarantine directories were never created

The installer creates the quarantine root but not the five subdirectories that
50-user writes to: clean, virus, spam, banned and bad_header. Whether this
surfaces depends on your quarantine configuration. When it does, Amavis rejects mail
outright because it has nowhere to write. The upgrade creates all five and corrects
their ownership.

Mailbox encryption was switched on against empty keys

The installer wrote a Dovecot configuration that enabled mailbox encryption
regardless of the database setting, which defaults to off, pointing at empty
placeholder key files. On affected gateways this breaks IMAP and SMTP
authentication. The upgrade re-renders the configuration from your database, so
whatever you actually chose is what you get.

Collaborative spam checks are now opt-in

Razor and Pyzor each transmit a digest of every scanned message to a third-party
network. That is a decision for the operator, so new installs now ship with them
disabled.

Your existing settings are not changed by this upgrade. If you had them enabled,
they stay enabled. Two things are worth knowing:

  • Razor has never been registered on any Hermes install, so it has been returning

no result whether or not it was switched on. Register it once under
Content Checks > Antispam Settings > Initialize Razor. Two separate faults
were in the way, and both are fixed: registration was writing to /root/.razor
while SpamAssassin reads /etc/razor, and the files it writes there were owned by
root while SpamAssassin runs as amavis. Razor writes its home directory at scan
time, not just at registration, so ownership had to be corrected too. Registering
now also removes the stale /root/.razor identity left behind by earlier attempts.

Nextcloud Mail was not provisioned when Nextcloud was enabled after the fact

Enabling Nextcloud on a mailbox that was created without it provisioned the account
and /nc access, but never the Mail profile, and the failure was silently discarded.
Affected users get a working Nextcloud login with an empty Mail app.

The upgrade repairs affected mailboxes automatically. If you configured an account
in Nextcloud Mail by hand to work around this, delete it first.
A hand-made account
looks like a working one, so the repair will skip that mailbox.

Nextcloud rejected the console address on fresh installs

The trusted-domain list was written comma-separated where Nextcloud expects it space
separated, producing one unusable entry. Reaching the console by IP, which is what the
installer configures before DNS exists, returned "Access through untrusted domain".
The upgrade re-renders it.

Changing the console address locked you out of the console

This one hit every new install, because the installer deliberately sets the console
address to the host IP (there is no DNS yet) and expects you to change it to your real
hostname afterwards. That change was the operation that broke.

Saving a new address restarted Authelia, whose session cookie is scoped to a single
hostname, so it instantly had no session for the address your browser was still on. The
save then handed your browser a redirect to the page that performs the Nginx restart, and
that page is itself behind authentication. It could not load, so Nginx was never
restarted and kept redirecting you to the old address with "unable to determine user
state". The configuration files on disk were correct the whole time. The only way back in
was restarting Nginx by hand:

docker container restart hermes_nginx

A console address change now performs the restart from the save itself, the last request
that is still authenticated, and shows you a page that waits for Nginx to come back
before moving you to the new address. Your next sign-in happens after the restart rather
than during it. Certificate, HSTS, OCSP and DH-parameter changes leave your session
intact and are unaffected.

If you are mid-change and locked out right now, run the command above, then open the
console at your new address.

Four block lists were counting refusals as listings

Four seeded entries shipped with no return-code filter: bl.spamcop.net,
bl.suomispam.net, bl.spameatingmonkey.net and backscatter.spameatingmonkey.net.
Without one, postscreen counts any answer in 127.0.0.0/8 as a listing, and that
range includes the 127.255.255.0/24 codes these lists return for "query refused" and
"over quota". A gateway whose DNS resolver is being refused therefore scored the
refusals as listings. Each of these carries a weight of 2 against a
postscreen_dnsbl_threshold of 3, so two of them answering with error codes reject
legitimate mail on their own.

The upgrade adds =127.0.0.[2..11] to all four, preserving whatever weight you had set.
No list is removed and no weight changes.

Related, and worth checking on your own gateway: the Test button under
Content Checks > RBL Configuration used to report any 127.x answer as healthy, including
those refusal codes, and reported a list with a live SOA record as healthy even when it
returned no data at all. It now distinguishes three outcomes: data returned, zone
present but silent, and refused or wildcarded. If your lists come back yellow after
this upgrade, they were never contributing, and the old button was telling you they
were fine.

Check whether your gateway receives block list answers at all

This is worth two minutes on every existing install. Ordinary DNS resolving normally
tells you nothing here, because reputation answers live in 127.0.0.0/8 and that is
exactly what upstream resolvers treat specially.

docker exec hermes_postfix_dkim dig +short 2.0.0.127.zen.spamhaus.org

| Answer | Meaning |
| --- | --- |
| 127.0.0.2 and similar | Working. Nothing to do |
| nothing at all | Your forwarder is discarding the answer, usually DNS rebinding protection refusing to relay loopback addresses |
| 127.255.255.254 | The list is refusing your resolver, which it treats as public or shared |

In the last two cases no reputation data is reaching the gateway: postscreen adds no
weight, SpamAssassin's RCVD_IN_* rules stay silent, and allowlists such as
list.dnswl.org stop applying, so every sender scores as though no reputation data
exists. Spam that should be quarantined gets delivered.

The fix is to switch to recursive resolution under System > DNS Resolver, then
re-test with Test All under Content Checks > RBL Configuration. Forward mode remains the
install default because it works on networks that block outbound port 53, and recursive
requires that port be open to the DNS root servers. New installs now run this same check
automatically and report the result.

Two default block lists changed

b.barracudacentral.org is no longer seeded on new installs. It only answers once
the querying IP is registered with Barracuda, so on a stock gateway it returned nothing
while carrying a weight of 7, which is above the rejection threshold of 3 on its own.
Your existing entry is left alone, on the same principle as your spam settings. If
you have not registered, remove it under Content Checks > RBL Configuration. If you have,
keep it.

Fresh installs also stop shipping dnsbl.sorbs.net, ix.dnsbl.manitu.net and
bl.mailspike.net in the Postfix template. SORBS was retired in 2024, and the other two
were already absent from the database, so a fresh install was filtering against a list
that differed from the one shown in the admin console. The two lists now match.

Authenticated users were rejected by your own DMARC policy

If your domain publishes p=reject, your own users could not send from a mail client. They
authenticated successfully and were then rejected at the end of the message with
550 5.7.1 rejected by DMARC policy.

The submission listeners inherited the global milter chain, which includes OpenDMARC. That
made DMARC evaluate authenticated outbound mail as though it had arrived from the internet,
where it fails by construction: the sending address is a laptop or a phone, which is never in
your SPF record, and the signing OpenDKIM instance on that path does not verify, so there is
no DKIM result either. With RejectFailures on, a p=reject policy then rejected the mail.

DMARC is an inbound check and no longer runs on submission. Port 25 is unchanged.

This was invisible before this release only because submission itself was never enabled.

The malware feed from URLhaus had silently stopped updating

The URLhaus signature set was capped at 2MB and the feed has grown past 3MB, so fangfrisch
refused it on every run. It exited successfully while doing so, which means the scheduler
recorded the job as fine and nothing surfaced. The ClamAV third-party URLhaus signatures were
simply never refreshed.

The limit is now 10MB. If you have tuned this yourself the upgrade leaves your value alone.

The scheduler could not deliver its own failure notifications

hermes_ofelia was not attached to the mail network, so the address it uses to send job
failure alerts could not be resolved or reached. Jobs ran correctly, which is why this went
unnoticed: the only broken part was the alerting, and alerting is only exercised when
something fails.

Local DNS records could take DNS down

Adding any record under System > DNS Resolver > Local DNS Records wrote a configuration
file that Unbound refused to parse when forwarding is enabled, which is the default. Unbound
then failed to start and crash-looped, taking DNS down for every container. Because the
console needs DNS, the admin UI could not be used to undo it.

The same page also rejected hostnames containing an underscore, which excluded _dmarc,
_domainkey and the _submission._tcp records this product tells you to publish, and could
not store a TXT value containing a semicolon, which is every real SPF, DKIM and DMARC record.

All three are fixed.

CipherMail logged and stamped mail in UTC

The CipherMail container had no timezone database, so it ran on UTC regardless of your
configured timezone and wrote +0000 (America) into the Received: header of every message
it handled. Its log lines were also offset from every other container, which makes tracing a
message across the pipeline misleading. The same omission affected the LDAP container.

Other fixes

  • Amavis shipped a default trusted network that was not the Docker subnet, so a fresh install

trusted an unrelated private range until an administrator saved a Postfix settings page.

  • Four administrative pages showed a raw error instead of the normal error page when they hit

a validation failure.

  • system_update_docker.sh --remote now selects the container registry and image tag as well

as the code, so one flag means one source. The per-release image tag is applied only if the
registry actually has it, so an upgrade that works today cannot start failing.

What to do

cd <install-root>
sudo ./scripts/system_update_docker.sh

Then, once, do this to finish applying the malware feed fix:

1. Open Content Checks > Malware Feeds.
2. Save the page without changing anything.

The size limit lands in the database during the upgrade, but the fangfrisch configuration file
is rendered from the database, and saving is what re-renders it. Until then the old limit
stays in effect and URLhaus keeps failing quietly. Confirm with:

docker logs hermes_ofelia 2>&1 | grep urlhaus | tail -2

You want INFO: ... updated, not ERROR: ... size exceeds defined limit.

Then, once, do this to finish applying the block list fix:

1. Open Content Checks > RBL Configuration.
2. Click the blue Edit (pencil) button on any entry.
3. Save it without changing anything.

That looks odd, and it is: the return-code fix lands in the database during the upgrade,
but Postfix only picks up a changed postscreen_dnsbl_sites when that directive is
re-rendered, and saving an entry is what triggers the re-render and the Postfix reload.
Saving any other Postfix-backed settings page, such as System > Server Setup, does the
same thing. Until one of those happens, main.cf keeps the old unfiltered list.

Confirm it applied. This should print nothing, meaning every entry now carries a return
code:

docker exec hermes_postfix_dkim postconf -h postscreen_dnsbl_sites | tr ',' '\n' | grep -v '='

Everything else in this upgrade applies on its own: the affected configuration is
re-rendered from your database, missing directories are created, ownership is corrected,
and the two one-time repairs run.

How to confirm it worked

# Quarantine directories exist and belong to amavis
docker exec hermes_mail_filter ls -ln /mnt/data/amavis

SpamAssassin configuration has no unsubstituted placeholders

docker exec hermes_mail_filter grep -c 'USE-' /etc/spamassassin/local.cf # expect 0

Bayes has been reset and is ready to learn

docker exec hermes_mail_filter sa-learn --dump magic | grep -E 'nspam|nham'

Nextcloud trusts your console address as its own entry

docker exec -u www-data hermes_nextcloud php /var/www/html/occ config:system:get trusted_domains

Mailbox encryption should be absent from the Dovecot configuration unless you enabled
it under Email Server > Settings:

grep crypt_write_algorithm config/dovecot-2.4/conf/dovecot.conf

Every block list carries a return-code filter, and none is left bare. After clicking
Apply, the running configuration should agree with the database:

# No entry without an '=' return code
docker exec hermes_postfix_dkim postconf -h postscreen_dnsbl_sites | tr ',' '\n' | grep -v '='

If Razor is enabled, confirm registration landed where SpamAssassin reads it and that
amavis can use it. An identity file in /etc/razor owned by amavis, and nothing
left in /root/.razor, is the correct end state:

docker exec hermes_mail_filter ls -la /etc/razor
docker exec -u amavis hermes_mail_filter sh -c 'touch /etc/razor/.t && rm -f /etc/razor/.t && echo WRITABLE || echo NOT-WRITABLE'

Why it happened

The installer renders Dovecot and SpamAssassin configuration before the database
exists
, so it can only mirror the seeded defaults. Where it wrote something the
database disagreed with, the file stayed wrong until an administrator saved the
relevant settings page, at which point the application layer silently corrected it.
Any gateway that had ever been configured through the console looked healthy.

Fresh installs are tested, but that testing verified containers were running and the
database was populated. It never authenticated to IMAP or pushed a message through
Amavis, so every one of these defects sat in the gap between those two things.

The quarantine directories and the ownership pass both existed in the pre-Docker
installer and were dropped during the Docker rewrite.

What changed

  • scripts/install_hermes_docker.sh: trusted-domain list is space separated; mailbox

encryption mirrors the database default; SpamAssassin configuration is rendered
rather than copied; the five quarantine subdirectories are created; new
post-container ownership pass for the quarantine tier and the Bayes corpus.

  • config/database/hermes_install.sql: network checks and Bayes auto-learning seed

disabled for new installs; both v260807 repairs pre-seeded as complete so a fresh
install never runs them.

  • schedule/post_upgrade.cfm: idempotent config re-renders that run on every upgrade,

plus the two one-time repairs.

  • inc/edit_mailbox_action.cfm: Nextcloud Mail is provisioned with a purpose-minted

system app password instead of the account's login password, which Dovecot could
never have accepted. Provisioning failures are now reported instead of discarded, and
saving a mailbox again is a valid repair.

  • Documentation: getting started and antispam settings pages rewritten around what a

fresh install actually looks like.

Known follow-up

  • Bayes needs roughly 200 spam and 200 ham before it contributes any score. On a quiet

gateway that takes a while. This is SpamAssassin's own safety threshold.

  • Automatic Bayes learning stays off by default. It trains on the rule set's verdicts,

so it reinforces their mistakes as readily as their successes.

Hermes SEG v260731 — scheduled jobs, update checker and version reporting

v260731 August 1, 2026

Hermes SEG v260731

Read this first

Apply this update even if your system looks fine. The problems it fixes are
silent — nothing on your dashboard reported them, and that was part of the fault.

Four scheduled jobs have not been running

On affected systems these were never started. There was no error, no alert, and
no indication in the console:

| Job | What it does | Consequence while stopped |
|---|---|---|
| hermes-fangfrisch-refresh | Refreshes ClamAV third-party malware signatures | Supplementary virus signatures went stale |
| hermes-dmarc-report | Generates DMARC aggregate reports | No reports sent to domains you receive mail from |
| hermes-health-check-mailqueue | Watches the mail queue, alerts on backlog | A growing queue raised no alarm |
| hermes-authelia-log-rotate | Rotates Authelia logs | Logs grew unbounded |

Core mail flow was not affected. Filtering, spam scanning, virus scanning
against ClamAV's own official signature feed, encryption and delivery all
continued working normally throughout.

Your update checker has been dead

The job that checks for new Hermes SEG releases was calling a script removed
several releases ago. It failed silently every night and never wrote its result,
so:

  • System Info → Hermes Update showed UPDATE CHECK PENDING permanently
  • No update notification e-mails were ever sent
  • You were never told that v260628, v260630, v260722 or v260723 existed

This is why you are reading this in a direct message rather than seeing a prompt
in your console.

Your version number was probably wrong

Any system installed fresh (rather than upgraded into) reported the version
of the installer baseline instead of its own. A fresh v260723 install displayed
v260612 on the dashboard and in the footer. The software was correct; only
the reported number was wrong.

---

What to do

On your Hermes SEG host:

cd /path/to/hermes-seg          # the directory you installed into
sudo ./scripts/system_update_docker.sh

Confirm the prompt. One run catches you up through every release you missed
you do not need to run it repeatedly or apply releases in order. Mail flow is not
interrupted. No manual database step, no image rebuild.

How to confirm it worked

sudo ./scripts/hermes_smoke_test.sh

Look for these three lines, and no failures:

OK    ofelia_jobs: 10 active job(s)
OK    config.ini matches ofelia_jobs (10 job(s) scheduled)
OK    hermes-update-check calls check_for_update.cfm

Then check System Info in the console: the Build should match the release
you are on, and Hermes Update should no longer read PENDING.

To see the update check work immediately rather than waiting for the nightly run:

docker exec hermes_commandbox curl -s http://localhost:8888/schedule/check_for_update.cfm

---

Why it happened

config/ofelia/config.ini — the file that tells the scheduler which jobs to run —
is generated from the ofelia_jobs database table by the admin console. It was
also, mistakenly, committed to the repository as a fixed copy taken from one machine
at the v260612 release, and Docker mounts that copy directly over the live file.

Nothing regenerated it at install time, and the upgrade process overwrote the live
file with the repository copy on every run. A system that had accidentally repaired
itself (by saving any SPF, DKIM, DMARC or certificate settings page, which triggers
a regeneration) was silently returned to the broken state at its next upgrade.

That frozen copy predated four of the ten scheduled jobs and still referenced the
old update-check script.

Separately, the installer wrote a fixed version number into new databases, and only
the upgrade process ever advanced it — so fresh installs kept the baseline value.

What changed

  • The schedule is now generated, not shipped. A new headless endpoint renders

config.ini from ofelia_jobs; the installer runs it at the end of setup and the
update orchestrator runs it during every upgrade, which also permanently closes
the overwrite problem. Available standalone as
./scripts/install_hermes_docker.sh --render-ofelia.

  • The version number is derived, not hardcoded — taken from the release in the

installed source tree, so it cannot drift again.

  • The system checks itself. hermes_smoke_test.sh now fails when the scheduled

jobs disagree with the database, naming the jobs that are not running, and verifies
the update check points somewhere real. It previously reported "All critical checks
passed"
on a system with four dead jobs, because it counted database rows rather
than what was actually scheduled.

  • The dashboard tells the truth. An unverified update reading now carries a warning

and an explanation, plus a Check now action. Previously UPDATE CHECK PENDING
looked identical in weight to a healthy LATEST VERSION.

  • Upgrade fixes — for existing systems, the database is repaired before the

schedule is generated from it, so a system missing job entries gets them back.

  • Also fixed: an installer version check that had silently returned nothing since

v260612, and an update-orchestrator bug where command-line options (--dry-run,
--yes, --remote, an explicit target version) were discarded partway through
the run.

Known follow-up

  • config/ofelia/config.ini is still tracked in the repository so the scheduler is

valid at first container start. The upgrade now corrects it immediately afterwards;
removing it entirely is the cleaner end state.

  • Container images are published only as :latest — no per-release image tags exist,

so a release cannot be pinned or rolled back at the image level. Version resolution
is guarded against this, and installs continue to work.

---

Issue #288. For
how Hermes SEG is released and upgraded, see
docs/install/release-and-update-methodology.md.

Hermes SEG v260723 — quarantine notifier flood fix

v260723 July 23, 2026

Hermes SEG v260723

> ## Upgrade impact: low — no manual step required
>
> This is a small patch on top of v260722. It ships a
> code-only fix to the quarantine notifier — no schema change, no new
> container or image
, no Console Settings / nginx regeneration. The update
> orchestrator deploys the new file and advances build_no automatically.

This release follows v260722.

Fixes

Quarantine notifier no longer floods the queue after a migration/restore (#287)

The near-real-time quarantine notifier (schedule/quarantine_notify.cfm, #180)
runs every 60s and selected messages to notify about using only
msgrcpt.notification_sent = 0 — it had no age filter. Because that column is
a Docker-era addition the legacy database predates, the v260722 legacy→Docker
migration's additive schema-forward added it DEFAULT 0, stamping every restored
historical quarantine row
as "never notified." The notifier then treated the
entire quarantine history as brand-new and drained it at 100/min — observed as
49,000+ [Quarantine Notice] messages piling up in the queue. (The v260722
outbound-delivery pause contained the blast so nothing left the box, but the queue
still filled.)

Fixed in two layers so the whole class of bug is closed, not just this migration:

  • Systemic backstopschedule/quarantine_notify.cfm gained a 7-day recency

guard (msgs.time_num, using the existing msgs_idx_time_num index). The
notifier can no longer notify on messages older than the window regardless of
the flag
, so any path that reintroduces old msgrcpt rows at 0 (cross-host
restore/DR rehost, manual DB import, a future migration build) can never flood.

  • Fix at the sourcescripts/migrate_legacy_to_docker.sh now marks restored

historical quarantine as already-handled (notification_sent = 1) right after the
schema-forward step, so a fresh migration never generates the backlog in the first
place. Idempotent; warns rather than aborts on failure.

Trade-off of the recency guard: if the notifier (or the box) is down longer than the
window, quarantines that age past 7 days are silently skipped — acceptable for a
courtesy notice.

Already-flooded box? Mark the history handled and clear the stale queue:

docker exec -i hermes_db_server mariadb -u root hermes \
  -e "UPDATE msgrcpt SET notification_sent = 1 WHERE ds IN ('B','D') AND notification_sent = 0;"

Confirm the queue is the quarantine notices, then delete ONLY those (by postmaster

sender — NOT postsuper -d ALL):

docker exec hermes_postfix_dkim postqueue -p | head

Upgrading

1. Update the code. Git-based (e.g. Test): git fetch && git reset --hard v260723,
or run the standard orchestrator (scripts/system_update_docker.sh v260723).
2. No schema changeupdates/v260723/sql/schema_updates.sql only advances
build_no (the orchestrator's version stamp). The notifier fix takes effect as
soon as the new .cfm is in place.
3. Verify: Mail Queue still shows the Outbound Delivery card; the every-60s
quarantine notifier no longer regenerates notices for old quarantined mail.

---

Issue: #287
(related: #180,
#150). For how
Hermes is released and upgraded, see
docs/install/release-and-update-methodology.md.

Hermes SEG v260722 — Pause Outbound Delivery + migration hardening

v260722 July 22, 2026

Hermes SEG v260722

> ## Upgrade impact: low — no manual step required
>
> This release adds a small, idempotent schema change (the Pause Outbound
> Delivery
control) that the update orchestrator applies automatically. There is
> no Console Settings / nginx regeneration step and no new container or image.
> Existing installs upgrade with the normal command and are done.

This release follows v260630.

What's new

Pause / Resume Outbound Delivery (Mail Queue)

The Mail Queue page gains an Outbound Delivery card showing ACTIVE or
PAUSED, with Pause / Resume buttons. Pausing holds all outbound mail in
the queue; resuming releases the hold and flushes the queue.

  • The hold is a first-class defer_transports parameters directive (not a bare

postconf), so it survives every subsequent Save & Apply — including the SPF/
DKIM/DMARC saves — instead of being silently un-paused by the next config render.

  • Default is normal delivery (enabled=0); existing behavior is unchanged until

an administrator explicitly pauses.

Fixes

Legacy → Docker migration hardening (#150)

scripts/migrate_legacy_to_docker.sh was hardened extensively against real
build-240815 backups. Each of these was a live failure found by running the
migration end-to-end:

  • Safe-cutover hold — a freshly migrated box now comes up with outbound

HELD, so it cannot blast the restored quarantine's stale notifications to real
recipients. A completion banner and a Release Outbound Delivery checklist step
make the hold explicit. (This is the migration side of the Pause control above.)

  • Postfix directives resolved after restore — backfills parameters.parent_name

and merges in the baseline parameters seed rows the legacy DB predates, so
SPF/DKIM/DMARC Save & Apply no longer fails with bad numerical configuration.

  • Mail flows after restore — compiles the postfix hash: maps the live config

references (was smtpd-451-rejecting every message) and chowns the restored
quarantine to the container's amavis uid (was deferring on Permission denied).

  • Strict-mode safe & fail-loud — the schema-forward step runs cleanly under

MariaDB 11.4 strict sql_mode and reports a clear error instead of aborting
silently on failure.

DKIM key generation on fresh installs (#285)

Fresh installs of v260612 / v260628 / v260630 failed to generate DKIM keys
(opendkim-genkey … chdir(): No such file) because dkim/keys/ was absent in a
fresh clone. Fixed for fresh clones, fresh installs, and already-deployed hosts.

Postfix master.cf re-injection (public snapshot drift)

The public repo's master.cf snapshot predated #232 (loopback-bound :10026,
no_milters), which broke CipherMail cross-container re-injection and disabled
post-MIME-rebuild DKIM re-signing on fresh installs / migrations. Restored to the
#232 design (matches production). Installs built before the public repo already
had the correct file and are unaffected.

Repository / tooling

  • Removed hardcoded install-root paths repo-wide (scripts self-locate; admin pages

use the live Docker directory), so installs rooted at any path work — including two
admin pages that had printed a wrong path in copy-pasteable instructions.

  • Added a pre-commit guard (Layer 4) that blocks hardcoded install roots in staged

code, plus a fresh-install smoke-test path fix (#284).

Upgrading

1. Update the code. Git-based (e.g. Test): git fetch && git reset --hard v260722,
or run the standard orchestrator (scripts/system_update_docker.sh v260722).
2. Schema applies automaticallyupdates/v260722/sql/schema_updates.sql adds the
defer_transports directive rows (idempotent) and advances build_no.
3. Verify: Mail Queue → the Outbound Delivery card shows ACTIVE.

---

Issues: #150,
#284,
#285. For how Hermes
is released and upgraded, see
docs/install/release-and-update-methodology.md.

Hermes SEG v260630

v260630 June 30, 2026

Hermes SEG v260630

> ## ⚠️ ACTION REQUIRED AFTER UPGRADING
>
> This release adds a public landing page at the console root (/) and moves
> the login portal to /auth. These changes live in the nginx templates, so
> existing installs must regenerate their nginx configuration to activate them.
>
> ➜ After upgrading, an administrator must open
> System → Console Settings and click _Save & Apply Settings_ once.

> No fields need to change — saving re-renders both nginx configs from the updated
> templates and reloads nginx.
>
> Nothing breaks if you skip it — the console keeps its current behavior — but
> the new landing page and login routing will not appear until you do.

>
> Behavioral change once regenerated: visiting the bare console URL
> (https://<your-host>/) now shows a landing page with links to the User and
> Admin consoles, instead of going straight to the Authelia login. The login screen
> moves to /auth. Direct links to /admin, /users, /nc/, /ciphermail, etc.
> are unchanged and continue to work exactly as before.

This release follows v260628.

What's new

Public landing page at the console root (#283)

Browsing the bare console hostname now lands on a clean splash page instead of an
immediate login prompt:

  • User Console is the prominent call-to-action — for mailbox and relay users

to review quarantined mail, manage their account & security, and (mailbox users)
access webmail.

  • Admin Console is a deliberately secondary link for administrators.

Login is functionally unchanged — it simply moves to /auth so the root URL can host
the landing page. Every existing entry point (/admin, /users, /nc/, /ciphermail)
behaves exactly as before.

Friendlier direct /nc/ (webmail) access

Typing the Nextcloud URL (/nc/) directly while logged out previously dead-ended on a
Nextcloud "Page not found" (a known OIDC URL-mangling quirk when the session cookie
isn't primed first). Cold /nc/ visits are now routed to the User Console, where the
Webmail link opens Nextcloud cleanly via the normal primed flow.

Upgrading

This release ships no schema changes and no new containers — but it does
require the nginx regeneration step from the top of this document.

1. Update the code. Git-based (e.g. Test): git fetch && git reset --hard <tag>,
or run the standard orchestrator (scripts/system_update_docker.sh).
2. Regenerate nginx (required): System → Console Settings → Save & Apply
Settings
. This re-renders hermes-ssl.conf + auth.conf from the updated
templates and reloads nginx.
3. Verify: https://<host>/ → landing page · /admin + /users/auth login ·
/nc/ (logged out) → User Console.

> Why a manual step? Hermes renders its live nginx config from templates via the
> Console Settings / host-configuration flow; routine upgrades do not currently
> re-render it automatically. A future release will fold this into the post-upgrade
> hook (schedule/post_upgrade.cfm) so the step becomes automatic.

---

Issue: #283. For how Hermes is released and upgraded, see
docs/install/release-and-update-methodology.md.

Hermes SEG v260628 — Let's Encrypt in Community Edition

v260628 June 28, 2026

Hermes SEG v260628

> Maintenance + feature release. Headline change: Let's Encrypt / ACME
> certificate management is now available in Community Edition
— automated
> issuance and renewal are no longer Pro-gated. Plus documentation refinements
> and a published project changelog. No schema changes; no mandatory action for
> existing installs.

This release follows v260612, the initial public Docker
release. It is a small, low-risk update — primarily an edition-gating change for
Let's Encrypt and a batch of documentation improvements.

What's new

Let's Encrypt / ACME is now Community (#282)

The entire Let's Encrypt / ACME certificate gate has moved to all editions:

  • System Certificates → Request ACME Certificate — issue and auto-renew a free

Let's Encrypt certificate for the console / mail host. Previously Pro-only.

  • Email Server → Domains → Auto-managed (Let's Encrypt) — per-mailbox-domain

SAN certificates with automatic DNS/IP validation, issuance, and renewal.
Previously Pro-only.

Free, automated TLS is table-stakes for a self-hosted mail platform, so it now ships
to everyone. The certbot engine, SAN validator, and SMTP-SNI generator all run on
Community. The manual CSR + import path remains for those who prefer a commercial CA.

Existing Pro installations are unaffected — they already had this functionality;
nothing is removed or downgraded.

Documentation & housekeeping

  • Added CHANGELOG.md in Keep a Changelog format.
  • README refinements: corrected container count, refreshed hero descriptor and the

Link Guard row, removed the Pro pricing badge.

  • Get-started guide: added admin-email review, Antispam Maintenance (Pyzor / Razor /

Bayes), Barracuda registration, and CipherMail console-password steps.

  • Fixed the System Users edit-modal Access-Control-Policy documentation link.

Upgrading

This is a drop-in update — there are no schema changes and no new containers.

  • Docker (git-based, e.g. Test): git fetch && git reset --hard <tag> then redeploy

the web files, or run the standard update orchestrator
(scripts/system_update_docker.sh).

  • Pro Edition: the Pro template fingerprint changed in this release (two

fingerprinted templates were edited for the ACME gating). The validation server's
manifest for v260628 has been published, so upgrading Pro boxes validate normally —
no action required. Boxes that stay on v260612 also continue to validate against
their existing manifest.

Edition matrix (certificates)

| Capability | Community | Pro |
| --- | :---: | :---: |
| Import 3rd-party certificate / Generate CSR | ✅ | ✅ |
| Request ACME (Let's Encrypt) — console cert | ✅ (new) | ✅ |
| Auto-managed SAN certs per mailbox domain | ✅ (new) | ✅ |

---

Issue: #282.
For how Hermes is released and upgraded, see
docs/install/release-and-update-methodology.md.

Hermes SEG v260612 — Initial Public Docker Release

v260612 June 20, 2026

Hermes SEG v260612 — Initial Public Docker Release

> Early-adopter release. v260612 is the first public release of Hermes SEG's
> Docker era — feature-complete and validated end-to-end on our DEV and Test
> environments. If you run a production gateway, stand v260612 up in parallel and
> put it through your own acceptance tests before cutting over
— send and receive
> mail through it, exercise your relay flows, quarantine release, DKIM/SPF/DMARC
> alignment, and your backup/restore plan. Field feedback from early adopters
> directly shapes the next release.

This is the first tagged public release of Hermes SEG as a Docker product. Hermes
shipped for years as a bare-metal Ubuntu install — a custom installer, host-level
Postfix / Amavis / Dovecot / Lucee / OpenLDAP, host-managed systemd services.
v260612 marks the Dockerized rewrite as a coherent, shipping product: a 19-container
stack orchestrated by Docker Compose, a five-tier storage topology, a single-command
update orchestrator, Authelia SSO across the admin console + user portal + Nextcloud,
time-of-click link protection, Docker-aware backup / disaster-recovery tooling, and a
release pipeline built on GitHub Releases and ghcr.io.

This release is fresh-install only — legacy-to-Docker migration tooling exists in
skeletal form but is not yet end-to-end Docker-aware (see Migrating from legacy).

Summary

  • Early-adopter release — feature-complete and validated on our infrastructure;

exercise your real mail flow in parallel before cutting a production gateway over.

  • Full Docker stack: 19 containers, single docker compose up -d, all services run

in containers (no host-level mail stack). Replaces the legacy bare-metal installer.

  • Time-of-click Link Guard (Pro): inbound links are rewritten through a Hermes

redirect and checked for reputation at click time — closing the gap where a link
is weaponized after delivery.

  • Docker-aware backup, disaster recovery & re-host: hot backups, cross-host restore

with storage-topology remap and credential reconciliation, and a host-identity rewire.

  • Five-tier storage topology: Config / Data / Archive / Vmail / Nextcloud, each

independently mountable so you put each tier on the right kind of disk.

  • Single-command update orchestrator: scripts/system_update_docker.sh runs a

5-phase pipeline (git pull → image pull → per-release artifacts → finalize →
post-upgrade hook), auto-resolves the latest tag via the GitHub Releases API, and
auto-runs occ upgrade + rehydrates Nextcloud apps on NCVERSION bumps.

  • Authelia SSO: unified MFA (TOTP / WebAuthn / Duo Push) across admin console, user

portal, and Nextcloud (via OIDC).

  • Nextcloud integrated: webmail, file sync, calendars (CalDAV), and contacts

(CardDAV) ship with the stack, pre-provisioned on first OIDC login.

  • GitHub-distributed: images at ghcr.io/deeztek/hermes-<service>:<tag>; releases on

GitHub; code on GitLab for dev.

---

What's included

Link Guard — time-of-click safe links (Pro Edition)

Inbound email links are rewritten to route through a Hermes-hosted redirect, and the
destination's reputation is checked at the moment the user clicks — closing the gap
where a link is weaponized after delivery.

  • Dedicated container hermes_linkguard (self-contained Python service) runs in the

compose stack. The body milter rewrites links to a UI-configured base URL pointing at
the in-stack /lg/ endpoint. (Running the container off-box on a separate host is
deferred to a later release.)

  • Layered verdict pipeline: heuristics (lookalike / punycode / IP-literal / @ /

URL-shortener / excess-subdomain) + open-redirect detection + free local feeds
(URLhaus, OpenPhish) + an operator-managed list of abused cloud-storage / redirector
hosts + optional Google Safe Browsing / VirusTotal + optional guarded
redirect-chain following — each reputation source individually enabled/disabled from
one toggle list, behind a verdict cache.

  • Admin-configured per-tier actions: clean → redirect; suspicious → warn / allow /

block; malicious → block / block-with-override / warn.

  • Protected-domain picker: choose from the recipient domains the box actually hosts

(or _default for all); URL allow/block rules with input validation.

  • Outbound restoration (default ON): when a user replies to or forwards a protected

message, the wrappers are unwrapped on the way out, so external recipients get clean
original links.

  • Rotatable HMAC signing key with a current+previous overlap window, so links already

in mailboxes keep working through a rotation.

  • Every link is protected regardless of length — rewritten links use a compact

server-side reference, so no link is skipped for being too long.

  • Diagnostics in the admin UI: a Check a URL tool shows the verdict, which layer

decided it, and the resolved host; a Recent activity view lists recent click
evaluations (host only, for privacy).

  • New Pro-gated admin page: Email Policies → Link Guard.

Mail flow

  • Outbound disclaimers (Pro): per-domain / per-address disclaimer templates, applied

by the dedicated hermes_body_milter Python container in the Postfix milter chain.

  • Personal signatures (Community): rich-HTML per-user signatures with a Quill editor,

template gallery, tables, and social-media icons, rendered on every outbound message.

  • Organizational signatures (Pro): admin-managed per-domain signature templates with

placeholder substitution (name / title / phone / email / department / org).

  • External Sender Banner (Community): inbound mail from outside the org gets a visual

banner injected by hermes_body_milter.

  • CID inline image support in body modifiers: signatures and disclaimers can embed

inline images that survive multipart/related wrapping and DKIM signing.

  • OpenARC integration: hermes_openarc does ARC chain signing on outbound and

verification on inbound for forwarding-trust preservation.

  • Multi-instance OpenDKIM: separate signer and verifier instances so outbound-sign and

inbound-verify behave independently without config bleed.

Mailbox hosting

  • Email Server section — Domains / Mailboxes / Aliases / Mailbox Rules with a full

admin UI on top of Dovecot 2.4.

  • Dovecot 2.4 custom Ubuntu-based image, replacing 2.3.x (breaking config/plugin/ACL

changes absorbed).

  • Shared mailboxes + user-managed folder sharing (Dovecot ACL / vfile), admin and user

UIs, Rebuild ACL Files action.

  • Sieve rules: admin global rules + per-user filters, on an isolated dovecot_sieve

volume.

  • Vacation auto-reply with per-user date scoping and per-mailbox timezone.
  • BCC Maps UI: sender-BCC and recipient-BCC management.
  • Mobile device setup wizard: "Set Up Your Devices" walkthroughs, signed iOS

.mobileconfig (IMAP/SMTP/CalDAV/CardDAV), QR-gated download.

  • App Passwords: unified credential system for Dovecot IMAP/SMTP + Nextcloud DAV.
  • Email autoconfiguration: autodiscover + autoconfig endpoints from SNI certificates;

CalDAV/CardDAV autodiscovery.

Nextcloud integration

  • Webmail + Files + Calendar + Contacts out of the box, SSO via Authelia OIDC.
  • user_oidc-based integration with a pre-provisioning pipeline that creates NC

accounts on first login, plus NC Mail profiles so users land in working webmail.

  • External Sites integration: a "User Console" link in the NC top menu points back to

the Hermes user portal, kept in sync with console-hostname changes.

  • Vendor-driven version management: NCVERSION is Hermes-release-managed; each bump

ships only after passing the NC integration check on a Test box.

  • Maintenance Mode card in System Settings for NC-native admin access (local

username/password + TOTP) independent of SSO.

Operations: backup, disaster recovery & upgrades

  • Docker-aware backup & restore: hot (zero-downtime) backups, scoped/slim storage

tiers, a directory-style backup format, streamed restore, disk-space pre-checks, and
email notifications (--notify-email, --notify-on-success).

  • Cross-host disaster recovery + re-host: restore a backup onto fresh hardware —

system_restore.sh auto-remaps the storage topology when it differs from the source,
reconciles per-service DB credentials (including Nextcloud's config.php) to the
target host's own creds/, and detects a cross-host restore and offers to run
system_rehost.sh, which rewires host identity (console hostname, regenerated service
configs, Nextcloud OIDC discovery + end-session URLs). A version-match gate guards
against accidental cross-version restores. After any restore, follow the
Post-Restore Steps.

  • Self-healing update orchestrator: system_update_docker.sh self-heals tracked-file

runtime/restore drift (saves a recovery patch, then git checkout -f) instead of
refusing, has a pre-container pre-scripts/ hook + self-re-exec, and only restarts what
changed.

  • Let's Encrypt cert store in backups: ACME certs (config/certbot/conf/,

symlink-preserving) survive a cross-host restore; the nginx vhost generator falls back
to the bootstrap cert when a domain's LE cert isn't present, so a config regen can't
emit a missing-cert path and crash nginx.

Security & administration

  • Authelia SSO with an LDAP backend; TOTP / WebAuthn / Duo Push across console,

portal, and Nextcloud.

  • Per-domain MFA enforcement with per-mailbox override; app passwords for clients that

can't do MFA.

  • Pro Edition licensing: Pro features require a valid license to remain active; if a

license expires or is revoked, Pro functionality is disabled until restored, and free
(Community) functionality is unaffected. Validation hits validate.hermesseg.io over
HTTPS and is cached locally so Pro stays available during brief network outages.

  • Content filtering: Amavis + SpamAssassin + ClamAV, custom message rules, per-rule

score overrides, custom file-type rules, quarantine + release.

  • Encryption: CipherMail-based S/MIME + PGP, PDF encryption / portal reply.

---

Editions: Community vs Pro

Community Edition is fully functional and needs no license. Pro Edition unlocks seven
advanced features:

| Pro Feature | What it does |
|---|---|
| Let's Encrypt (ACME) automation | Automated issuance + renewal of free Let's Encrypt certs for the console and per-domain. Community can still request/use LE certs manually. |
| Email disclaimers | Per-domain outbound disclaimer templates at the milter level. |
| Organizational signatures | Centrally-managed per-domain employee signatures with placeholder substitution. Community has Personal Signatures (per-user) only. |
| Intrusion Prevention (IPS) | Web UI for Fail2ban jails, ban thresholds, whitelists, and a real-time view of active bans. The hermes_fail2ban container runs on all editions; Pro gates the UI + which jails are active. |
| Console firewall | Web UI for the host firewall protecting the admin console (port allowlisting, source-IP restriction). |
| LDAP RemoteAuth | Per-domain pass-through authentication to external LDAP / Active Directory; auto-provisions mailboxes on first successful login; supports STARTTLS and LDAPS. |
| Link Guard (safe links) | Time-of-click URL protection for inbound mail (see above). |

---

Installing

sudo git clone https://github.com/deeztek/Hermes-Secure-Email-Gateway.git
cd Hermes-Secure-Email-Gateway
sudo ./scripts/install_hermes_docker.sh

The installer runs in a single session, 10–30 minutes (mostly image downloads). It will:

1. Display the Pro EULA and ask for acceptance.
2. Prompt for mail server hostname (FQDN), console address, host IP, upstream DNS
forwarders, and the four storage mount paths.
3. Generate all secrets and per-service config files (LDAP secrets, DB passwords, Authelia
session keys, OIDC keypair, self-signed bootstrap cert, Docker secret files).
4. Write DATA_MOUNT / ARCHIVE_MOUNT / VMAIL_MOUNT / FILES_MOUNT into .env.
5. Run docker compose up -d --build to pull images and start the stack.
6. Initialize all databases (Hermes, Authelia, Nextcloud, OpenDMARC, CipherMail, Syslog),
populate the LDAP base structure, create the initial admin user, and pre-provision the
Nextcloud admin.
7. Print an installation summary with the admin console URL and one-time admin credentials.

The installer is idempotent — re-running it on an already-installed system skips
completed work via state guards. Run --help for the full flag list.

After install you have:

  • Admin Console: https://<console-host>/admin/
  • User Portal: https://<console-host>/users/
  • Nextcloud: https://<console-host>/nc/

The bootstrap admin lands you in a working console, but mail won't flow until you
complete the minimum first-run config in
docs/install/get-started-docker.md — first domain,
relay networks, first recipient or mailbox, DNS records. The dashboard surfaces two nudges
(Placeholder hostname, Self-signed cert) that auto-clear when satisfied.

Configuring Link Guard (optional, Pro)

Link Guard is dormant until enabled — safe to install ahead of configuring it.

1. Email Policies → Link Guard (Pro Edition).
2. Set the Redirect base URL (e.g. https://<console-host>/lg/).
3. Add protected recipient domains (or _default for all).
4. Choose per-tier actions; toggle reputation sources (URLhaus/OpenPhish free;
Google Safe Browsing / VirusTotal need API keys); optionally add URL allow/block
rules.
5. Enable Link Guard and Save & Reload. The first save generates the signing keys
and pushes config to the milter and the container.
6. Send a test inbound message and confirm links resolve correctly at click time.

---

System requirements

| Resource | Requirement |
|---|---|
| CPU | 4 vCPUs minimum; more for higher mail volume |
| RAM | 8 GB minimum, 16 GB+ recommended for production |
| Disk | See below |

Hermes splits storage across five independent tiers, so disk sizing depends on your
layout:

  • Production (each tier on its own disk): ~120 GB for the OS / Config disk (OS, Docker

engine, the full Hermes image set + running containers, the repo, install/service logs),
plus a dedicated disk per data tier — Data, Archive, Vmail, Nextcloud
each sized to your mail and file storage needs. The tiers have deliberately different I/O
and growth profiles, so isolating them lets you match disk to workload and expand each
independently.

  • Small or test (everything on one disk): point Archive, Vmail, and Nextcloud at the

same path as Data; ~275 GB total (thin-provisioned) is a comfortable starting point.

See docs/install/storage-topology.md for the
canonical reference.

| Tier | Default path | Contents | Profile |
|---|---|---|---|
| 1. Config | install root (implicit) | Repo working tree, config/ subtrees, secrets, .env | Fast SSD, modest size |
| 2. Data | /mnt/data | All databases, Amavis state, ClamAV signatures, Lucee home, Sieve scripts, all logs, OpenDMARC, Postfix queue | Fast SSD; DB growth + log retention |
| 3. Archive | /mnt/archive | Amavis quarantine archive | Cheap bulk; retention × inflow |
| 4. Vmail | /mnt/vmail | Dovecot mailboxes | Cheap bulk; users × quota |
| 5. Nextcloud | /mnt/files | Nextcloud app + user files + NC Redis cache | Cheap bulk; user file storage |

---

Migrating from legacy

Honest status: a skeletal migration script exists at
scripts/migrate_legacy_to_docker.sh (restores
legacy DBs into the Docker MariaDB, creates the Docker-only DBs, copies legacy config
trees). It does not yet auto-detect the legacy 3-tier storage layout or migrate
Authelia users to LDAP automatically. Recommendation: treat v260612 as fresh-install
only. If you run a production legacy install, run a parallel Docker install on a second
host and evaluate before cutting over by hand. The legacy bare-metal system_backup.sh /
system_restore.sh scripts are not safe to run against a Docker install.

Known limitations / things you'll want to know

  • Post-upgrade browser hard-refresh. After an upgrade that bumps NCVERSION or changes

admin web assets, hard-refresh open admin/Nextcloud tabs (Ctrl-Shift-R / Cmd-Shift-R) —
the browser often serves the pre-upgrade CSS/JS bundle.

  • No CLI recovery path for admin lockout yet. If you misconfigure the console hostname

or host IP and lock yourself out of the admin UI, recover from a hypervisor/VM snapshot;
a menu-driven scripts/hermes-cli.sh recovery tool is planned for a future release.

  • Docker subnet is pinned. IPV4SUBNET (default 172.16.32) is referenced across 15+

config files; do not change it after install — there is no current template path to
propagate the change everywhere. Dynamic subnet support is on the backlog.

  • Vendored CipherMail binary. The vendored CipherMail .deb / .tar.xz files exceed

GitHub's 50 MB size-warning threshold. Functional, flagged for cleanup.

  • Nextcloud admin via Maintenance Mode. NC admin tasks needing local-NC auth go through

System → Settings → Nextcloud Maintenance Mode, not the SSO path.

Repository / distribution

  • Code: GitLab — https://gitlab.deeztek.com/dedwards/hermes-seg-docker-gl.git
  • Distribution: GitHub — https://github.com/deeztek/Hermes-Secure-Email-Gateway
  • Container images: GitHub Container Registry — ghcr.io/deeztek/hermes-<service>:<tag>
  • Issues + Releases: GitHub
  • Documentation: docs.deeztek.com

See docs/install/release-and-update-methodology.md
for the full release/upgrade methodology.

---

Welcome to the Docker era of Hermes SEG.

Hermes SEG build-240815

build-240815 June 28, 2026

Historical pre-Docker release (Ubuntu bare-metal lineage), originally released 2024-09-30. Browse this point in the project's history on the legacy branch. _Backfilled; GitHub stamps the publish date as today._

Hermes SEG build-231130

build-231130 June 28, 2026

Historical pre-Docker release (Ubuntu bare-metal lineage), originally released 2024-08-16. Browse this point in the project's history on the legacy branch. _Backfilled; GitHub stamps the publish date as today._