entrypoint: /data/opendkim removed, as planned for the release after 2.8.0
2.8.x copied the keys from /data/opendkim to /data/dkim and kept the old tree for a rollback to 2.7.x "until the next release" (docs/plans/panel-dkim-signing.md, Q2); the owner's "потом режь 2.9.0" (2026-10-16) makes this that release. The entrypoint now removes the tree once /data/dkim holds a key, and says so. A /data with keys under the old tree only - one that never ran 2.8.x - stops the start with a message to run 2.8.x first, rather than let the panel give the server's own name a new key its published record would not match; the tree is left as it was. Gone with it: the copy into /data/dkim.new, the tree's normalisation, its exemption from the general hand-over, its key link in drop_links, and the backup's exclusion of "opendkim" (dead: the panel never sees the tree now). Tests: e2e TestStartsOnA28Data replaces TestStartsOnA27Data - a /data as 2.8.x left it (both trees, a queue of foreign uids, a rotation due): the old tree is gone, the key under /data/dkim is the one left, the selector table names it, the queue is Postfix's by name; TestRefusesA27Data - the start stops, says why, leaves the old key and makes no new one. go vet + go test (26 packages) and make e2e (all pass) on the test server for this tree. Docs: guide § Upgrading "To 2.9.0" (rollback to 2.8.x needs nothing, to 2.7.x only from a 2.7.x backup; the 2.7.x rollback paragraph says it is for 2.8.x only and keeps naming 2.7.0), guide § backup, architecture (the old tree, § Persistence's table and the restore line), security § Backup, development's e2e paragraph, the plan's key step; CHANGELOG [Unreleased] § Removed. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -31,6 +31,18 @@ Format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/); version
|
|||||||
Health, which nothing on Settings pointed to. `POST
|
Health, which nothing on Settings pointed to. `POST
|
||||||
/server/settings/dns-recheck`, global role.
|
/server/settings/dns-recheck`, global role.
|
||||||
|
|
||||||
|
### Removed
|
||||||
|
|
||||||
|
- **`/data/opendkim`, the key tree of 2.7.x** (panel-dkim-signing, as
|
||||||
|
planned for the release after 2.8.0): 2.8.x copied its keys to
|
||||||
|
`/data/dkim` and kept it for a rollback to 2.7.x; the first start of
|
||||||
|
this image removes it once `/data/dkim` holds a key, and says so. A
|
||||||
|
`/data` whose keys are under the old tree only - one that never ran
|
||||||
|
2.8.x - stops the start rather than give the server's own name a new
|
||||||
|
key: start 2.8.x once first (guide, § Upgrading). The copy and the
|
||||||
|
backup's exclusion of the tree went with it. A rollback from here goes
|
||||||
|
to 2.8.x; to 2.7.x only through a 2.7.x backup.
|
||||||
|
|
||||||
## [2.8.0] - 2026-10-16
|
## [2.8.0] - 2026-10-16
|
||||||
|
|
||||||
The panel signs outgoing mail with DKIM itself, and OpenDKIM - a 2018 beta
|
The panel signs outgoing mail with DKIM itself, and OpenDKIM - a 2018 beta
|
||||||
|
|||||||
+20
-34
@@ -87,7 +87,7 @@ drop_links() {
|
|||||||
done
|
done
|
||||||
}
|
}
|
||||||
drop_links /data/dkim /data/dkim.new /data/opendkim /data/sasl /data/postfix /data/antispam /data/log \
|
drop_links /data/dkim /data/dkim.new /data/opendkim /data/sasl /data/postfix /data/antispam /data/log \
|
||||||
/data/dkim/keys /data/dkim/selectors /data/opendkim/keys \
|
/data/dkim/keys /data/dkim/selectors \
|
||||||
/data/sasl/sasldb2 /data/postfix/queue \
|
/data/sasl/sasldb2 /data/postfix/queue \
|
||||||
/data/postfix/sender_login_maps /data/postfix/relay_domains \
|
/data/postfix/sender_login_maps /data/postfix/relay_domains \
|
||||||
/data/postfix/transport /data/postfix/relay_recipients /data/postfix/tls_policy \
|
/data/postfix/transport /data/postfix/relay_recipients /data/postfix/tls_policy \
|
||||||
@@ -96,51 +96,37 @@ drop_links /data/dkim /data/dkim.new /data/opendkim /data/sasl /data/postfix /da
|
|||||||
# under /data; make sure they stay writable without disturbing anything that a
|
# under /data; make sure they stay writable without disturbing anything that a
|
||||||
# later phase deliberately hands to another service. /data/log is exempt: it is
|
# later phase deliberately hands to another service. /data/log is exempt: it is
|
||||||
# deliberately owned by postfix (postlogd writes the delivery log there) and is
|
# deliberately owned by postfix (postlogd writes the delivery log there) and is
|
||||||
# normalised on its own below, as are the trees shared through group selfpost
|
# normalised on its own below, as are the trees shared through group selfpost.
|
||||||
# and the one OpenDKIM read until 2.8.0, which is left as it was (below).
|
|
||||||
# A file's owner is a number: an entry whose user or group is not the panel's
|
# A file's owner is a number: an entry whose user or group is not the panel's
|
||||||
# by that number - unpacked by root, or written under another image whose
|
# by that number - unpacked by root, or written under another image whose
|
||||||
# image gave the panel other ids (999:999 until 2.7.0, 10999:10999 since) - is
|
# image gave the panel other ids (999:999 until 2.7.0, 10999:10999 since) - is
|
||||||
# handed over whole.
|
# handed over whole.
|
||||||
find /data -mindepth 1 -maxdepth 1 ! -name log ! -name postfix ! -name opendkim ! -name sasl ! -name antispam \
|
find /data -mindepth 1 -maxdepth 1 ! -name log ! -name postfix ! -name sasl ! -name antispam \
|
||||||
\( ! -user panel -o ! -group panel \) -exec chown -R panel:panel {} +
|
\( ! -user panel -o ! -group panel \) -exec chown -R panel:panel {} +
|
||||||
|
|
||||||
# DKIM keys and the selector table, /data/dkim: the panel's alone - it makes
|
# DKIM keys and the selector table, /data/dkim: the panel's alone - it makes
|
||||||
# the keys, writes the table and signs (internal/domain, Keys; internal/milter,
|
# the keys, writes the table and signs (internal/domain, Keys; internal/milter,
|
||||||
# sign.go). Until 2.8.0 the keys were under /data/opendkim, shared with
|
# sign.go). The selector table is written by the panel before its signer
|
||||||
# OpenDKIM through group selfpost. That tree is kept for an image of 2.7.x
|
# serves (cmd/panel/signer.go). Until 2.8.0 the keys were under
|
||||||
# rolled back to (docs/plans/panel-dkim-signing.md, Q2), but nothing of
|
# /data/opendkim, shared with OpenDKIM; 2.8.x copied them to /data/dkim at its
|
||||||
# 2.8.0 reads it beyond the copy below: it is the panel's alone like the new
|
# first start and kept the old tree for a rollback to 2.7.x until the next
|
||||||
# one - a 2.7.x start gives it back to group selfpost itself - and a backup
|
# release, this one (docs/plans/panel-dkim-signing.md, Q2). It is removed
|
||||||
# leaves it out (internal/backup, excludedFromArchive). While /data/dkim
|
# once /data/dkim holds a key. A /data that never ran 2.8.x - keys under the
|
||||||
# holds no key and the old tree does, the keys are copied over. The copy is
|
# old tree only - stops the start instead: the panel would make the server's
|
||||||
# made as the panel user, which can read nothing there the panel could not,
|
# own name a new key, and its published record would no longer match; 2.8.x
|
||||||
# into a directory beside, renamed into place when it is whole: a start that
|
# copies them (guide § Upgrading). rm -rf removes a link, not what it points
|
||||||
# stops midway copies again. A copy that fails stops the start (set -e) - the
|
# to, and follows none inside (GNU coreutils rm(1)); a link at the path is
|
||||||
# panel would otherwise make the server's own name a new key, and its
|
# gone already (drop_links). (e2e TestStartsOnA28Data, TestRefusesA27Data.)
|
||||||
# published record would no longer match. The selector table is written by
|
|
||||||
# the panel before its signer serves (cmd/panel/signer.go). After a rollback
|
|
||||||
# to 2.7.x, a domain added there has its key only in the old tree: its
|
|
||||||
# directory is copied into /data/dkim/keys by hand (guide § Upgrading).
|
|
||||||
# (util-linux 2.41, setpriv(1): --reuid, --regid, --clear-groups; cp(1)
|
|
||||||
# -P: never follow a symbolic link in the source; chown -R and find never
|
|
||||||
# follow one either. GNU chmod keeps a directory's set-group-ID bit under a
|
|
||||||
# numeric mode unless it is cleared by name - "g-s" - and 2.7.x set it on
|
|
||||||
# the old tree: e2e TestStartsOnA27Data.)
|
|
||||||
has_keys() { [ -d "$1" ] && [ -n "$(find "$1" -type f -name '*.private' | head -n 1)" ]; }
|
has_keys() { [ -d "$1" ] && [ -n "$(find "$1" -type f -name '*.private' | head -n 1)" ]; }
|
||||||
if [ -d /data/opendkim ]; then
|
if [ -d /data/opendkim ]; then
|
||||||
chown -R panel:panel /data/opendkim
|
if ! has_keys /data/dkim/keys && has_keys /data/opendkim/keys; then
|
||||||
find /data/opendkim -type d -exec chmod 0700 {} + -exec chmod g-s {} +
|
echo "entrypoint: the DKIM keys are only under /data/opendkim, as 2.7.x kept them; start 2.8.x once first - it copies them to /data/dkim - then this image (guide, § Upgrading)" >&2
|
||||||
find /data/opendkim -type f -exec chmod 0600 {} +
|
exit 1
|
||||||
|
fi
|
||||||
|
rm -rf /data/opendkim
|
||||||
|
echo "entrypoint: removed /data/opendkim, the key tree of 2.7.x; the keys are under /data/dkim" >&2
|
||||||
fi
|
fi
|
||||||
rm -rf /data/dkim.new
|
rm -rf /data/dkim.new
|
||||||
if ! has_keys /data/dkim/keys && has_keys /data/opendkim/keys; then
|
|
||||||
setpriv --reuid=panel --regid=panel --clear-groups -- \
|
|
||||||
sh -c 'mkdir /data/dkim.new && cp -RP /data/opendkim/keys /data/dkim.new/keys'
|
|
||||||
rm -rf /data/dkim
|
|
||||||
mv /data/dkim.new /data/dkim
|
|
||||||
echo "entrypoint: the DKIM keys were copied from /data/opendkim to /data/dkim; /data/opendkim is left for a rollback" >&2
|
|
||||||
fi
|
|
||||||
mkdir -p /data/dkim/keys
|
mkdir -p /data/dkim/keys
|
||||||
chown -R panel:panel /data/dkim
|
chown -R panel:panel /data/dkim
|
||||||
find /data/dkim -type d -exec chmod 0700 {} + -exec chmod g-s {} +
|
find /data/dkim -type d -exec chmod 0700 {} + -exec chmod g-s {} +
|
||||||
|
|||||||
+9
-11
@@ -309,14 +309,13 @@ then OpenDKIM did, in a program of its own
|
|||||||
lines; nothing is written to `mail.log` (nor did OpenDKIM write there).
|
lines; nothing is written to `mail.log` (nor did OpenDKIM write there).
|
||||||
- **The old tree,** `/data/opendkim` - the keys, `KeyTable` and
|
- **The old tree,** `/data/opendkim` - the keys, `KeyTable` and
|
||||||
`SigningTable` until `2.8.0`, shared with OpenDKIM through group
|
`SigningTable` until `2.8.0`, shared with OpenDKIM through group
|
||||||
`selfpost`. While `/data/dkim` holds no key and the old tree does, the
|
`selfpost`. `2.8.x` copied its keys to `/data/dkim/keys` at its first
|
||||||
entrypoint copies `/data/opendkim/keys` to `/data/dkim/keys` as the panel
|
start and kept it for a rollback to a 2.7.x image; since `2.9.0` the
|
||||||
user (`setpriv`), into `/data/dkim.new`, renamed when whole; a copy that
|
entrypoint removes it once `/data/dkim` holds a key, and a `/data` with
|
||||||
fails stops the start - the server's name would otherwise get a new key
|
keys under the old tree only - one that never ran 2.8.x - stops the start
|
||||||
and its published record would no longer match. Keys, selectors and DNS
|
(the server's name would otherwise get a new key and its published record
|
||||||
records do not change. The old tree is left for a rollback to a 2.7.x
|
would no longer match). Under 2.8.x it was the panel's alone, like the
|
||||||
image until the next release, made the panel's alone at every start like
|
new one, and left out of backups.
|
||||||
the new one, and left out of backups (§ Persistence).
|
|
||||||
|
|
||||||
### Panel binary ([cmd/panel](../cmd/panel))
|
### Panel binary ([cmd/panel](../cmd/panel))
|
||||||
|
|
||||||
@@ -1100,7 +1099,6 @@ single-connection trade-off that follows from it.
|
|||||||
| `selfpost.db` | SQLite panel state (users, sessions, domains, apps, send log, L2 limits, log-tailer offset, inbound relay, DMARC reports) — see [Database](#database) and [schema-migrations.md](schema-migrations.md) |
|
| `selfpost.db` | SQLite panel state (users, sessions, domains, apps, send log, L2 limits, log-tailer offset, inbound relay, DMARC reports) — see [Database](#database) and [schema-migrations.md](schema-migrations.md) |
|
||||||
| `setup-token` | First-run setup token file |
|
| `setup-token` | First-run setup token file |
|
||||||
| `dkim/` | DKIM keys (`keys/<name>/<selector>.private`) and the selector table (`selectors`) the signer reads; the panel's alone, `0700` / `0600` — § DKIM signing |
|
| `dkim/` | DKIM keys (`keys/<name>/<selector>.private`) and the selector table (`selectors`) the signer reads; the panel's alone, `0700` / `0600` — § DKIM signing |
|
||||||
| `opendkim/` | The key tree OpenDKIM read until `2.8.0` (keys, `KeyTable`, `SigningTable`): copied to `dkim/` once, then kept only for a rollback to 2.7.x until the next release; the panel's alone, excluded from backups |
|
|
||||||
| `sasl/sasldb2` | Application SASL credentials |
|
| `sasl/sasldb2` | Application SASL credentials |
|
||||||
| `postfix/sender_login_maps` | Login → From binding |
|
| `postfix/sender_login_maps` | Login → From binding |
|
||||||
| `postfix/relay_domains` | Inbound domains accepted on port 25 |
|
| `postfix/relay_domains` | Inbound domains accepted on port 25 |
|
||||||
@@ -1180,8 +1178,8 @@ the stored log lines follow;
|
|||||||
`postfix reload` in `postrotate` — see § Log tailer above).
|
`postfix reload` in `postrotate` — see § Log tailer above).
|
||||||
|
|
||||||
**Restore:** panel button or `selfpost-backup` CLI — self-contained archive:
|
**Restore:** panel button or `selfpost-backup` CLI — self-contained archive:
|
||||||
`data/` (SQLite snapshot + tree minus `log/`, the setup token, the old
|
`data/` (SQLite snapshot + tree minus `log/`, the setup token and any
|
||||||
`opendkim/` tree and any `tls/` under `/data`), `docker-compose.yml`, `.env`,
|
`tls/` under `/data`), `docker-compose.yml`, `.env`,
|
||||||
and `certs/` when present;
|
and `certs/` when present;
|
||||||
version check on restore. Requires the project directory mounted read-only at
|
version check on restore. Requires the project directory mounted read-only at
|
||||||
`SELFPOST_DEPLOY_ROOT` (`/selfpost-deploy` in the default compose file). On the
|
`SELFPOST_DEPLOY_ROOT` (`/selfpost-deploy` in the default compose file). On the
|
||||||
|
|||||||
+6
-5
@@ -503,12 +503,13 @@ record as a receiver would. `signature_over_awkward_headers`
|
|||||||
([test/e2e/dkim_shapes_test.go](../test/e2e/dkim_shapes_test.go)) sends the
|
([test/e2e/dkim_shapes_test.go](../test/e2e/dkim_shapes_test.go)) sends the
|
||||||
header shapes Postfix hands a milter in its own way - folded, tab-led, no
|
header shapes Postfix hands a milter in its own way - folded, tab-led, no
|
||||||
space after the colon, two `To:` - and an 8-bit body, and dkimpy verifies
|
space after the colon, two `To:` - and an 8-bit body, and dkimpy verifies
|
||||||
what was delivered. `TestStartsOnA27Data`
|
what was delivered. `TestStartsOnA28Data`
|
||||||
([test/e2e/upgrade_test.go](../test/e2e/upgrade_test.go)) starts the image on
|
([test/e2e/upgrade_test.go](../test/e2e/upgrade_test.go)) starts the image on
|
||||||
a `/data` a 2.7.x image left: the key under `/data/opendkim` is copied to
|
a `/data` a 2.8.x image left: the old key tree `/data/opendkim` is removed,
|
||||||
`/data/dkim` unchanged and the selector table names it, the old tree becomes
|
the key under `/data/dkim` is kept unchanged and the selector table names
|
||||||
the panel's alone, and a queue owned by other uids is handed to Postfix's
|
it, and a queue owned by other uids is handed to Postfix's users by name;
|
||||||
users by name.
|
`TestRefusesA27Data` has the start stop on a `/data` whose keys are under
|
||||||
|
the old tree only, which is left as it was.
|
||||||
Waits poll with a timeout (`waitFor`); a fixed sleep is the exception and says
|
Waits poll with a timeout (`waitFor`); a fixed sleep is the exception and says
|
||||||
why where it stands (`sender_check_test.go`: a downtime worth a second or two,
|
why where it stands (`sender_check_test.go`: a downtime worth a second or two,
|
||||||
and 35 s to show a row is not written again).
|
and 35 s to show a row is not written again).
|
||||||
|
|||||||
+12
-4
@@ -684,7 +684,17 @@ was, for a rollback, until the next release, and which a full backup leaves
|
|||||||
out. The selector table, `data/dkim/selectors`, is written from the database
|
out. The selector table, `data/dkim/selectors`, is written from the database
|
||||||
at every start, before the signer serves.
|
at every start, before the signer serves.
|
||||||
|
|
||||||
**Rolling back to 2.7.x** after that: the 2.7.x image signs with OpenDKIM
|
**To 2.9.0** from 2.8.x there is nothing to do: the first start removes
|
||||||
|
`data/opendkim`, the key tree 2.7.x read, which 2.8.x kept for a rollback,
|
||||||
|
and says so (`entrypoint: removed /data/opendkim, the key tree of 2.7.x;
|
||||||
|
the keys are under /data/dkim`); the keys under `data/dkim` stay as they
|
||||||
|
are. From 2.7.x, start 2.8.x once first: an image of 2.9.0 on a `data`
|
||||||
|
whose keys are under `data/opendkim` only stops at the start and says so,
|
||||||
|
rather than give the server's own name a new key. Rolling back from 2.9.x
|
||||||
|
to 2.8.x needs nothing; to 2.7.x, the old tree is gone - only a full backup
|
||||||
|
of 2.7.x brings it back.
|
||||||
|
|
||||||
|
**Rolling back to 2.7.x** from 2.8.x (only): the 2.7.x image signs with OpenDKIM
|
||||||
from `data/opendkim`, which holds the keys as they were at the upgrade. A
|
from `data/opendkim`, which holds the keys as they were at the upgrade. A
|
||||||
domain added under 2.8.x has no key there, and 2.7.x sends its mail
|
domain added under 2.8.x has no key there, and 2.7.x sends its mail
|
||||||
unsigned; one deleted and added again under 2.8.x has its old key there,
|
unsigned; one deleted and added again under 2.8.x has its old key there,
|
||||||
@@ -771,9 +781,7 @@ as themselves, the host's IPv6 address and `127.0.0.1` as `172.18.0.1`.
|
|||||||
domains' DKIM keys under `data/dkim/`, all applications' SASL credentials,
|
domains' DKIM keys under `data/dkim/`, all applications' SASL credentials,
|
||||||
`manifest.json` with the version that created it), plus `docker-compose.yml`,
|
`manifest.json` with the version that created it), plus `docker-compose.yml`,
|
||||||
`.env`, and `certs/` from the operator directory next to `./data`. Delivery
|
`.env`, and `certs/` from the operator directory next to `./data`. Delivery
|
||||||
logs under `data/log/` are excluded, so is `data/opendkim/` (the key tree an
|
logs under `data/log/` are excluded, and so is the Postfix queue: the archive is taken while the
|
||||||
installation upgraded from 2.7.x keeps for a rollback, see
|
|
||||||
[Upgrading](#upgrading)), and so is the Postfix queue: the archive is taken while the
|
|
||||||
instance runs, and a copy of a queue Postfix is working on can hold a message
|
instance runs, and a copy of a queue Postfix is working on can hold a message
|
||||||
twice or not at all. Mail still waiting for delivery is therefore not carried
|
twice or not at all. Mail still waiting for delivery is therefore not carried
|
||||||
over, and a restored instance starts with an empty queue; to keep the queue,
|
over, and a restored instance starts with an empty queue; to keep the queue,
|
||||||
|
|||||||
@@ -171,6 +171,9 @@ dropped connection (a one-hour watchdog closes the signer).
|
|||||||
- [x] Keys: `/data/opendkim/keys` -> `/data/dkim/keys` (Q2): the entrypoint
|
- [x] Keys: `/data/opendkim/keys` -> `/data/dkim/keys` (Q2): the entrypoint
|
||||||
copies the tree once and leaves the old one for a rollback to 2.7.x until
|
copies the tree once and leaves the old one for a rollback to 2.7.x until
|
||||||
the next release; KeyTable and SigningTable go with the new layout.
|
the next release; KeyTable and SigningTable go with the new layout.
|
||||||
|
*2.9.0:* the entrypoint removes the old tree once `/data/dkim` holds a
|
||||||
|
key, and stops on a `/data` with keys under the old tree only; the copy
|
||||||
|
and the backup exclusion are gone.
|
||||||
- [x] Tests: `test/upstream/*.sh`, e2e's OpenDKIM log and process checks,
|
- [x] Tests: `test/upstream/*.sh`, e2e's OpenDKIM log and process checks,
|
||||||
the 256-byte test rewritten for the new rule.
|
the 256-byte test rewritten for the new rule.
|
||||||
|
|
||||||
|
|||||||
+4
-5
@@ -410,11 +410,10 @@ operator can act on it.
|
|||||||
private key from `certs/` when present; a domain export carries the DKIM key
|
private key from `certs/` when present; a domain export carries the DKIM key
|
||||||
and **working** application passwords in the clear (otherwise a transfer
|
and **working** application passwords in the clear (otherwise a transfer
|
||||||
without recreating credentials would be impossible). The keys a backup
|
without recreating credentials would be impossible). The keys a backup
|
||||||
carries are those of `/data/dkim`; `/data/opendkim`, the tree 2.7.x read
|
carries are those of `/data/dkim`. `/data/opendkim`, the tree 2.7.x read,
|
||||||
and kept for a rollback, is left out (`internal/backup`,
|
was left out while 2.8.x kept it for a rollback (`2.8.0`); since `2.9.0`
|
||||||
`excludedFromArchive`, `2.8.0`): an archive restores into its own version
|
the entrypoint removes it before the panel starts, so no second copy of
|
||||||
alone, and a second copy of every key - one that outlives a deleted
|
every key - one that outlived a deleted domain's - is left on disk.
|
||||||
domain's - has no place in it.
|
|
||||||
- Links under `certs/` (and `certs` itself as a link) are followed, in the
|
- Links under `certs/` (and `certs` itself as a link) are followed, in the
|
||||||
container's view: the archive carries whatever regular file they lead to
|
container's view: the archive carries whatever regular file they lead to
|
||||||
that the panel's user may read. `certs/` is the operator's, mounted
|
that the panel's user may read. `certs/` is the operator's, mounted
|
||||||
|
|||||||
@@ -105,11 +105,10 @@ const QueueDir = "postfix/queue"
|
|||||||
// fresh one is written instead); a "tls" directory under /data is skipped when
|
// fresh one is written instead); a "tls" directory under /data is skipped when
|
||||||
// an operator pointed TLS_CERT_FILE inside /data; and "log" is Postfix's raw
|
// an operator pointed TLS_CERT_FILE inside /data; and "log" is Postfix's raw
|
||||||
// delivery log plus its fourteen rotated files, which is diagnostic output, not
|
// delivery log plus its fourteen rotated files, which is diagnostic output, not
|
||||||
// state to restore, and by far the largest thing under /data. "opendkim" is the
|
// state to restore, and by far the largest thing under /data. (Until 2.9.0
|
||||||
// key tree OpenDKIM read until 2.8.0, kept on disk only for a rollback to 2.7.x:
|
// "opendkim" was left out too: the key tree OpenDKIM read until 2.8.0, kept on
|
||||||
// an archive restores into its own version alone, which reads the keys from
|
// disk for a rollback to 2.7.x; the entrypoint removes it before the panel
|
||||||
// /data/dkim, and a second copy of every key - one that outlives a deleted
|
// starts since.)
|
||||||
// domain's - has no place in it.
|
|
||||||
var excludedFromArchive = map[string]bool{
|
var excludedFromArchive = map[string]bool{
|
||||||
"selfpost.db": true,
|
"selfpost.db": true,
|
||||||
"selfpost.db-wal": true,
|
"selfpost.db-wal": true,
|
||||||
@@ -118,7 +117,6 @@ var excludedFromArchive = map[string]bool{
|
|||||||
"setup-token": true,
|
"setup-token": true,
|
||||||
"tls": true,
|
"tls": true,
|
||||||
"log": true,
|
"log": true,
|
||||||
"opendkim": true,
|
|
||||||
ManifestName: true,
|
ManifestName: true,
|
||||||
ContentsName: true,
|
ContentsName: true,
|
||||||
contentsCheckedName: true,
|
contentsCheckedName: true,
|
||||||
|
|||||||
@@ -44,9 +44,6 @@ func seedDataDir(t *testing.T) (dataDir, dbPath, deployRoot string) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
writeFile(t, filepath.Join(dataDir, "dkim", "keys", "example.com", "selfpost.private"), "PRIVATE KEY")
|
writeFile(t, filepath.Join(dataDir, "dkim", "keys", "example.com", "selfpost.private"), "PRIVATE KEY")
|
||||||
// The key tree OpenDKIM read until 2.8.0, left on disk for a rollback.
|
|
||||||
writeFile(t, filepath.Join(dataDir, "opendkim", "keys", "example.com", "selfpost.private"), "OLD KEY")
|
|
||||||
writeFile(t, filepath.Join(dataDir, "opendkim", "KeyTable"), "example.com example.com:selfpost:/data/opendkim/keys/example.com/selfpost.private")
|
|
||||||
writeFile(t, filepath.Join(dataDir, "sasl", "sasldb2"), "SASLDB")
|
writeFile(t, filepath.Join(dataDir, "sasl", "sasldb2"), "SASLDB")
|
||||||
writeFile(t, filepath.Join(dataDir, "postfix", "sender_login_maps"), "@example.com login")
|
writeFile(t, filepath.Join(dataDir, "postfix", "sender_login_maps"), "@example.com login")
|
||||||
writeFile(t, filepath.Join(dataDir, "antispam", "deny_domain.map"), "evil.example\n")
|
writeFile(t, filepath.Join(dataDir, "antispam", "deny_domain.map"), "evil.example\n")
|
||||||
@@ -144,8 +141,6 @@ func TestCreateIncludesStateExcludesTransient(t *testing.T) {
|
|||||||
DataArchivePrefix + "log/mail.log",
|
DataArchivePrefix + "log/mail.log",
|
||||||
DataArchivePrefix + "log/mail.log.1",
|
DataArchivePrefix + "log/mail.log.1",
|
||||||
DataArchivePrefix + "postfix/queue/deferred/sample",
|
DataArchivePrefix + "postfix/queue/deferred/sample",
|
||||||
DataArchivePrefix + "opendkim/keys/example.com/selfpost.private",
|
|
||||||
DataArchivePrefix + "opendkim/KeyTable",
|
|
||||||
} {
|
} {
|
||||||
if _, ok := files[name]; ok {
|
if _, ok := files[name]; ok {
|
||||||
t.Errorf("archive should not contain %s", name)
|
t.Errorf("archive should not contain %s", name)
|
||||||
|
|||||||
+85
-34
@@ -1,6 +1,7 @@
|
|||||||
package e2e
|
package e2e
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"context"
|
||||||
"crypto/rand"
|
"crypto/rand"
|
||||||
"crypto/rsa"
|
"crypto/rsa"
|
||||||
"crypto/x509"
|
"crypto/x509"
|
||||||
@@ -13,51 +14,53 @@ import (
|
|||||||
"time"
|
"time"
|
||||||
)
|
)
|
||||||
|
|
||||||
// TestStartsOnA27Data: the image starts on a /data an earlier image left -
|
// upgradeData is a /data an earlier image left, laid out for host: the
|
||||||
// the DKIM keys under /data/opendkim, as 2.7.x kept them, and a Postfix queue
|
// server's key under /data/dkim as 2.8.x keeps it (unless only27), the old
|
||||||
// owned by numbers that are not this image's postfix and postdrop. Without
|
// key tree /data/opendkim as 2.7.x kept it - 2.8.x left it for a rollback -
|
||||||
// opendkim installed the package accounts get other numbers (postfix 101 ->
|
// and, with a queue, a Postfix queue owned by numbers that are not this
|
||||||
// 100, postdrop 103 -> 102 in 2.8.0), and the first rollout of 2.8.0 to the
|
// image's postfix and postdrop, and a log rotation due at the start. It
|
||||||
// test server crash-looped on "postfix check" (2026-10-16): the queue is
|
// returns the directory and the key's PEM.
|
||||||
// handed to Postfix's users by name before the check now. And the old key is
|
func upgradeData(t *testing.T, host string, only27 bool) (string, []byte) {
|
||||||
// the one signing: copied, not made anew, so the published record holds
|
t.Helper()
|
||||||
// (build/entrypoint.sh; review of panel-dkim-signing, F5 - no test ran the
|
|
||||||
// copy).
|
|
||||||
func TestStartsOnA27Data(t *testing.T) {
|
|
||||||
const host = "mail.example.test"
|
|
||||||
dataDir := t.TempDir()
|
dataDir := t.TempDir()
|
||||||
if err := os.Chmod(dataDir, 0o755); err != nil {
|
if err := os.Chmod(dataDir, 0o755); err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
certDir := t.TempDir()
|
|
||||||
if err := writeSelfSignedCert(certDir+"/fullchain.pem", certDir+"/privkey.pem"); err != nil {
|
|
||||||
t.Fatalf("generate throwaway TLS cert: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// The server's key as 2.7.x kept it, and its KeyTable.
|
|
||||||
key, err := rsa.GenerateKey(rand.Reader, 2048)
|
key, err := rsa.GenerateKey(rand.Reader, 2048)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
pemKey := pem.EncodeToMemory(&pem.Block{Type: "RSA PRIVATE KEY", Bytes: x509.MarshalPKCS1PrivateKey(key)})
|
pemKey := pem.EncodeToMemory(&pem.Block{Type: "RSA PRIVATE KEY", Bytes: x509.MarshalPKCS1PrivateKey(key)})
|
||||||
keyFile := filepath.Join(dataDir, "opendkim", "keys", host, "selfpost.private")
|
write := func(rel string, data []byte) {
|
||||||
if err := os.MkdirAll(filepath.Dir(keyFile), 0o755); err != nil {
|
p := filepath.Join(dataDir, rel)
|
||||||
|
if err := os.MkdirAll(filepath.Dir(p), 0o755); err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
if err := os.WriteFile(keyFile, pemKey, 0o644); err != nil {
|
if err := os.WriteFile(p, data, 0o644); err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
if err := os.WriteFile(filepath.Join(dataDir, "opendkim", "KeyTable"),
|
}
|
||||||
[]byte(host+" "+host+":selfpost:/data/opendkim/keys/"+host+"/selfpost.private\n"), 0o644); err != nil {
|
write(filepath.Join("opendkim", "keys", host, "selfpost.private"), pemKey)
|
||||||
t.Fatal(err)
|
write(filepath.Join("opendkim", "KeyTable"), []byte(host+" "+host+":selfpost:/data/opendkim/keys/"+host+"/selfpost.private\n"))
|
||||||
|
if !only27 {
|
||||||
|
write(filepath.Join("dkim", "keys", host, "selfpost.private"), pemKey)
|
||||||
|
write(filepath.Join("dkim", "selectors"), []byte(host+" selfpost\n"))
|
||||||
}
|
}
|
||||||
|
|
||||||
// The owners and modes 2.7.x left, and a queue of foreign numbers, laid
|
// The owners and modes the earlier images left - 2.8.x made both key
|
||||||
// out as root in a throwaway container of the image.
|
// trees the panel's alone, 2.7.x the old one group selfpost's with the
|
||||||
|
// set-group-ID bit - and a queue of foreign numbers, laid out as root in
|
||||||
|
// a throwaway container of the image.
|
||||||
layout := `set -e
|
layout := `set -e
|
||||||
|
if [ -d /data/dkim ]; then
|
||||||
|
chown -R 10999:10999 /data/dkim /data/opendkim
|
||||||
|
find /data/dkim /data/opendkim -type d -exec chmod 0700 {} +
|
||||||
|
find /data/dkim /data/opendkim -type f -exec chmod 0600 {} +
|
||||||
|
else
|
||||||
chown -R 10999:10998 /data/opendkim
|
chown -R 10999:10998 /data/opendkim
|
||||||
find /data/opendkim -type d -exec chmod 2750 {} +
|
find /data/opendkim -type d -exec chmod 2750 {} +
|
||||||
find /data/opendkim -type f -exec chmod 0640 {} +
|
find /data/opendkim -type f -exec chmod 0640 {} +
|
||||||
|
fi
|
||||||
q=/data/postfix/queue
|
q=/data/postfix/queue
|
||||||
for d in active bounce corrupt defer deferred flush hold incoming private saved trace; do
|
for d in active bounce corrupt defer deferred flush hold incoming private saved trace; do
|
||||||
mkdir -p $q/$d; chown 4242:4243 $q/$d; chmod 700 $q/$d
|
mkdir -p $q/$d; chown 4242:4243 $q/$d; chmod 700 $q/$d
|
||||||
@@ -70,7 +73,26 @@ echo "Oct 14 00:00:00 mail postfix/master[1]: daemon started" > /data/log/mail.l
|
|||||||
printf 'logrotate state -- version 2\n"/data/log/mail.log" 2026-10-14-0:0:0\n' > /data/log/.logrotate.status`
|
printf 'logrotate state -- version 2\n"/data/log/mail.log" 2026-10-14-0:0:0\n' > /data/log/.logrotate.status`
|
||||||
if out, err := exec.Command("docker", "run", "--rm", "--entrypoint", "sh",
|
if out, err := exec.Command("docker", "run", "--rm", "--entrypoint", "sh",
|
||||||
"-v", dataDir+":/data", "selfpost:e2e", "-c", layout).CombinedOutput(); err != nil {
|
"-v", dataDir+":/data", "selfpost:e2e", "-c", layout).CombinedOutput(); err != nil {
|
||||||
t.Fatalf("lay out the 2.7 /data: %v\n%s", err, out)
|
t.Fatalf("lay out the earlier /data: %v\n%s", err, out)
|
||||||
|
}
|
||||||
|
return dataDir, pemKey
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestStartsOnA28Data: the image starts on a /data 2.8.x left - the keys
|
||||||
|
// under /data/dkim, the old tree /data/opendkim still there for a rollback to
|
||||||
|
// 2.7.x, which 2.9.0 ends - and a Postfix queue owned by numbers that are not
|
||||||
|
// this image's postfix and postdrop. The old tree is removed and the key
|
||||||
|
// under /data/dkim is the one kept, unchanged, so the published record holds.
|
||||||
|
// The queue is handed to Postfix's users by name before "postfix check": the
|
||||||
|
// first rollout of 2.8.0 crash-looped on it when removing opendkim shifted
|
||||||
|
// the package accounts' numbers (2026-10-16); a removed package can shift
|
||||||
|
// them again.
|
||||||
|
func TestStartsOnA28Data(t *testing.T) {
|
||||||
|
const host = "mail.example.test"
|
||||||
|
dataDir, pemKey := upgradeData(t, host, false)
|
||||||
|
certDir := t.TempDir()
|
||||||
|
if err := writeSelfSignedCert(certDir+"/fullchain.pem", certDir+"/privkey.pem"); err != nil {
|
||||||
|
t.Fatalf("generate throwaway TLS cert: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
name := "selfpost-e2e-upgrade"
|
name := "selfpost-e2e-upgrade"
|
||||||
@@ -106,20 +128,20 @@ printf 'logrotate state -- version 2\n"/data/log/mail.log" 2026-10-14-0:0:0\n' >
|
|||||||
t.Fatalf("%v\n%s", err, logs())
|
t.Fatalf("%v\n%s", err, logs())
|
||||||
}
|
}
|
||||||
|
|
||||||
if got := exec1(`cmp /data/opendkim/keys/` + host + `/selfpost.private /data/dkim/keys/` + host + `/selfpost.private && echo same`); got != "same" {
|
if got := exec1(`test -e /data/opendkim && echo there || echo gone`); got != "gone" {
|
||||||
t.Errorf("the key under /data/dkim is not the one copied: %s", got)
|
t.Errorf("/data/opendkim is %s after the start", got)
|
||||||
|
}
|
||||||
|
if got := exec1(`cat /data/dkim/keys/` + host + `/selfpost.private`); got != strings.TrimSpace(string(pemKey)) {
|
||||||
|
t.Error("the key under /data/dkim is not the one 2.8.x left")
|
||||||
}
|
}
|
||||||
if got := exec1(`cat /data/dkim/selectors`); got != host+" selfpost" {
|
if got := exec1(`cat /data/dkim/selectors`); got != host+" selfpost" {
|
||||||
t.Errorf("selector table = %q", got)
|
t.Errorf("selector table = %q", got)
|
||||||
}
|
}
|
||||||
if got := exec1(`stat -c '%U:%G %a' /data/opendkim/keys/` + host + `/selfpost.private /data/opendkim/keys`); got != "panel:panel 600\npanel:panel 700" {
|
|
||||||
t.Errorf("the old tree is not the panel's alone: %q", got)
|
|
||||||
}
|
|
||||||
if got := exec1(`stat -c '%U:%G' /data/postfix/queue/active /data/postfix/queue/maildrop /data/postfix/queue/pid`); got != "postfix:postfix\npostfix:postdrop\nroot:postfix" {
|
if got := exec1(`stat -c '%U:%G' /data/postfix/queue/active /data/postfix/queue/maildrop /data/postfix/queue/pid`); got != "postfix:postfix\npostfix:postdrop\nroot:postfix" {
|
||||||
t.Errorf("the queue is not Postfix's by name: %q", got)
|
t.Errorf("the queue is not Postfix's by name: %q", got)
|
||||||
}
|
}
|
||||||
if !strings.Contains(logs(), "the DKIM keys were copied from /data/opendkim to /data/dkim") {
|
if !strings.Contains(logs(), "entrypoint: removed /data/opendkim") {
|
||||||
t.Errorf("the entrypoint did not say it copied the keys:\n%s", logs())
|
t.Errorf("the entrypoint did not say it removed the old tree:\n%s", logs())
|
||||||
}
|
}
|
||||||
// A rotation due at the start runs before Postfix does: it rotates, and
|
// A rotation due at the start runs before Postfix does: it rotates, and
|
||||||
// its postrotate asks no reload of a Postfix not running yet (the test
|
// its postrotate asks no reload of a Postfix not running yet (the test
|
||||||
@@ -132,3 +154,32 @@ printf 'logrotate state -- version 2\n"/data/log/mail.log" 2026-10-14-0:0:0\n' >
|
|||||||
t.Errorf("the rotation at the start failed:\n%s", l)
|
t.Errorf("the rotation at the start failed:\n%s", l)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TestRefusesA27Data: a /data that never ran 2.8.x keeps the keys under
|
||||||
|
// /data/opendkim only. The start stops and says to run 2.8.x first, which
|
||||||
|
// copies them, rather than let the panel make the server's own name a new key
|
||||||
|
// that its published record would not match; the old tree is left as it was.
|
||||||
|
func TestRefusesA27Data(t *testing.T) {
|
||||||
|
const host = "mail.example.test"
|
||||||
|
dataDir, _ := upgradeData(t, host, true)
|
||||||
|
|
||||||
|
name := "selfpost-e2e-upgrade-27"
|
||||||
|
_ = exec.Command("docker", "rm", "-f", name).Run()
|
||||||
|
defer exec.Command("docker", "rm", "-f", name).Run()
|
||||||
|
ctx, cancel := context.WithTimeout(context.Background(), time.Minute)
|
||||||
|
defer cancel()
|
||||||
|
out, err := exec.CommandContext(ctx, "docker", "run", "--rm", "--name", name,
|
||||||
|
"-e", "SELFPOST_HOSTNAME="+host, "-v", dataDir+":/data", "selfpost:e2e").CombinedOutput()
|
||||||
|
if err == nil || ctx.Err() != nil {
|
||||||
|
t.Fatalf("the image started on a 2.7 /data (err %v, %v):\n%s", err, ctx.Err(), out)
|
||||||
|
}
|
||||||
|
if !strings.Contains(string(out), "the DKIM keys are only under /data/opendkim") {
|
||||||
|
t.Errorf("the refusal does not say why:\n%s", out)
|
||||||
|
}
|
||||||
|
if _, err := os.Stat(filepath.Join(dataDir, "opendkim", "keys", host, "selfpost.private")); err != nil {
|
||||||
|
t.Errorf("the old key is gone after the refusal: %v", err)
|
||||||
|
}
|
||||||
|
if _, err := os.Stat(filepath.Join(dataDir, "dkim", "keys", host)); err == nil {
|
||||||
|
t.Error("a key directory was made under /data/dkim before the refusal")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user