Repository navigation
Development - #29
Merged
Merged
Development#29
Conversation
OpenDKIM was installed and running since 0d30f56 (2020) but Postfix never talked to it: main.cf had no milter directives at all, and key.table, signing.table and trusted still held the yourdomain.tld placeholders. Outgoing mail was therefore never signed, which a Google DMARC report made visible -- auth_results contained no dkim element at all, DMARC passed on aligned SPF alone. With p=reject that breaks as soon as a message is forwarded and SPF falls away. Wire Postfix to the milter and make key management automatic: * main.cf.tpl gets smtpd_milters and non_smtpd_milters from DKIM_MILTER. non_smtpd_milters matters for locally injected mail (cron, PHP mail(), sendmail), which Postfix presents to the milter as localhost. milter_default_action = accept keeps mail flowing if opendkim is down. * The tables are now domain independent. key.table.tpl uses OpenDKIM's "%" substitution, which is replaced with the sender's domain in both the domain field and the key path, and signing.table is a catch-all. Adding a domain therefore only needs a key file -- no table rewrite, no reload. * dkim-sync.sh creates missing keys for every active domain in the Postfixadmin database plus anything in DKIM_DOMAINS, prints the TXT records to publish and stores them in the keys volume. dkim-watch.sh runs it periodically, so a domain added in the web UI is picked up without touching docker-compose.yml, and reminds the postmaster by mail once a day while a DNS record is still missing. Publication is checked by comparing the published p= value against the local key rather than by the exit status of opendkim-testkey, which also fails on zones that are not DNSSEC signed. * opendkim.conf: MTA ORIGINATING so SMTP AUTH submissions from external client IPs get signed instead of only verified -- master.cf already sets the macro, and InternalHosts stays limited to loopback on purpose. All On-* handlers are forced to accept: Mode sv also verifies incoming mail, and the shipped defaults reject on a malformed signature and tempfail on DNS or internal errors, which nothing here would benefit from. Canonicalization is now relaxed/relaxed so a mutated body does not break the signature. * Dockerfile: dns-root-data, because --no-install-recommends left TrustAnchorFile pointing at a missing file; dnsutils for the DNS comparison; sqlite3 moved out of the debug block since domain discovery depends on it. Create /etc/opendkim/keys and /var/run/opendkim, the latter being needed for the PidFile. * supervisord: opendkim before postfix, restart on unexpected exit, drop the pointless -D and the redundant user=, fix the stopwaitsecs typo, and add the control socket so dkim-sync.sh can restart opendkim after creating a key. Keys live in /etc/opendkim/keys and need a volume -- without one they are recreated with the container and the published DNS records silently stop matching, which is worse than not signing at all.
The workflow logged in with docker login -u -p and the DOCKER_USER / DOCKER_PASSWORD secrets, which do not exist on pull requests from forks, so those builds always failed. It also rebuilt everything from scratch each time and only ever produced the mutable :latest and :canary tags. Publish to ghcr.io/behringer24/mailship using GITHUB_TOKEN, which needs no secret at all, and keep pushing the same tags to Docker Hub in parallel. Pull requests now build without pushing. metadata-action adds an immutable :sha-<commit> tag to every build so a deployment can be pinned or rolled back, and it sets org.opencontainers.image.source, which is what links the package to this repository. Note: a GHCR package starts out private even for a public repository. After the first push its visibility has to be switched to public once, by hand, under Packages -> mailship -> Package settings. Also replace the two Docker Cloud badges, whose autobuild service no longer exists, with the Actions workflow badge.
VOLUME used the docker-compose "name:path" form. Docker reads every element of that JSON array as a container path, so it was declaring paths literally called "maildir:/var/vmail" and the four intended volumes were never declared at all. Note for anyone running without explicit mounts: these four paths now really are volumes, so Docker will create anonymous volumes for them. /etc/opendkim/keys is deliberately left out. An anonymous volume is a bad place for private key material because it is neither easy to find nor to back up -- mount it by name instead, as documented in the README.
The factory is addressed as module:callable, not module.callable. With the dot supervisord refused to start at all: Error: supervisor.rpcinterface.make_main_rpcinterface cannot be resolved within [rpcinterface:supervisor] so the container exited with code 2 immediately after make finished. Found by building and running the image locally.
The build was broken outright: deb.debian.org answers 404 for buster since Debian 10 went end of life, so apt-get update failed on the very first RUN and no image could be produced at all. Point apt at archive.debian.org, whose Release files are expired by definition, hence Acquire::Check-Valid-Until. packages.sury.org has dropped buster too, so PHP 7.4 is no longer obtainable from anywhere. Use Debian's own PHP 7.3 instead and follow that through everywhere the version appears: the package names, the pool.d path, the supervisord command and the fastcgi socket nginx talks to. Missing the last one would have left postfixadmin answering 502. That also removes the third party repository along with the deprecated apt-key call and the gnupg2 and apt-transport-https dependencies it needed. This keeps the image on an unsupported base that receives no security updates. It is the status quo rather than a regression, but the move to a current Debian release should follow soon -- note that envproc is Python 2 and Debian 11+ ships no Python 2, so that has to be solved as part of it.
Debian 10 reached end of life: the image could only be built by pointing apt at archive.debian.org with Check-Valid-Until disabled, and it was stuck on PHP 7.3. Bookworm removes that workaround and brings PHP 8.2, Dovecot 2.3.19 and Postfix 3.7. Deliberately not Debian 13: trixie ships Dovecot 2.4, which rejects every 2.3 configuration file and offers no automatic migration path, so it would mean rewriting the dovecot templates as well. Changes that the version bump forced: - envproc is now the Go port (behringer24/envprocgo). The Python original starts with "/usr/bin/env python" and Debian 12 has no "python" executable any more, because supervisor pulls in python3 only. The container would boot no further than the first template. Syntax and the abort on an unset variable are identical, so no template changed. - Postfixadmin 3.3.10 -> 3.3.16, the last release of the 3.3 branch and the first one that runs on PHP 8. Staying on 3.3 keeps the composer-less source archive and avoids the 4.0 database upgrade, which is known to be inconsistent with this branch (postfixadmin issue #971). Taken along while touching this: - Dovecot logged every submitted password in clear text to the container log via auth_debug_passwords and auth_verbose_passwords. Both are commented out now; auth_verbose stays on, so failed logins are still attributable by user name. - Dropped the procps/nano/less layer that carried a "remove in prod" comment. - SHELL with pipefail moved to the top, so a failing download in a pipe can no longer be masked by tar and produce a silently broken image. - mkdir -p /run/php, the directory already exists in this base image. Verified by building the image and booting a container: all six supervised services reach RUNNING, templates render, postfixadmin setup.php creates the schema (db version 1847) and login.php answers 200, an IMAP login against a mailbox row in the sqlite database succeeds, and no password appears in the container log.
…y aufgefallen
Beim Ausrollen des Debian-12-Images auf einen Server, dessen Volumes unter
Debian 10 entstanden sind, kamen zwei Probleme zutage.
1. Gruppenzugehoerigkeit im Postfix-Spool
Gruppen werden in Volumes numerisch gespeichert. Die Nummer, die unter
Debian 10 zu "postdrop" gehoerte, zeigt unter Debian 12 auf "postfix" --
maildrop und public gehoerten dadurch der falschen Gruppe. Das setgid
postdrop-Binary kann dann nicht schreiben, und jede lokal eingespeiste
Mail scheitert: DKIM-Benachrichtigungen an den Postmaster und Cron-Mail.
SMTP von aussen laeuft weiter, was den Fehler leicht uebersehen laesst.
Das chmod nach dem chgrp ist zwingend und nicht kosmetisch: Ein
Gruppenwechsel loescht die sticky- und setgid-Bits, auf die genau diese
zwei Verzeichnisse angewiesen sind. Im Deploy war nach dem Gruppenwechsel
das sticky-Bit von maildrop verschwunden.
"postfix set-permissions" waere das naheliegende Werkzeug und ist
absichtlich nicht verwendet: Es chownt auch Manpages, die dieses
Slim-Image nicht mitbringt, und bricht dort ab
("chown: cannot access '/usr/share/man/man1/mailq.1.gz'"). Als
Makefile-Schritt haette das den Container am Booten gehindert.
Beide Zeilen sind mit "-" versehen, damit ein Rechteproblem den
Mailserver nie am Start hindert -- Fehler werden weiter ausgegeben.
2. Fehlender stats-writer-Socket
Der Dovecot-LDA laeuft als vmail und meldet nach jeder Zustellung an
/run/dovecot/stats-writer. Der Socket gehoert standardmaessig root mit
Modus 0600, weshalb jede zugestellte Mail eine Fehlerzeile
"net_connect_unix(...) failed: Permission denied" hinterliess. Die
Zustellung selbst gelingt, nur die Statistik geht verloren -- zum Preis
einer Fehlerzeile pro Mail.
Gebaut und im Container geprueft:
- alle sechs Dienste erreichen RUNNING, make laeuft fehlerfrei durch
- die Spool-Verzeichnisse stehen auf postfix:postdrop mit 1730 und 2710
- Reparatur belegt: Gruppe von Hand auf postfix gesetzt, nach einem
Neustart wieder postdrop, sticky- und setgid-Bit intakt
- der Socket existiert als srw-rw---- vmail vmail, doveconf -n bestaetigt
die Uebernahme des Blocks
- eine Zustellung ueber sendmail endet mit status=sent, die Nachricht
liegt im Maildir, und die Statuszeile enthaelt jetzt "Info: msgid=..."
anstelle der bisherigen stats-writer-Fehlermeldung
Der Container terminiert kein TLS -- das macht der Reverse Proxy davor --,
weshalb nginx hier nur Klartext-HTTP sieht und $https leer bleibt. PHP
haelt die Verbindung damit fuer unsicher: Session-Cookies bekommen kein
"secure"-Flag, und absolute URLs werden mit http:// gebaut, was Browser
als Mixed Content blockieren. Postfixadmin weist darauf sogar selbst hin
("connection not secure, switch to https if possible").
Geloest wird das auf der nginx-Ebene statt in jeder Anwendung einzeln:
Eine map leitet HTTPS aus dem X-Forwarded-Proto des Proxys ab. Damit
profitiert jede PHP-Anwendung im Container -- Postfixadmin ebenso wie ein
spaeter hinzukommender Webmailer. Roundcubes eigene Option use_https
koennte dasselbe, aber nur fuer Roundcube; Postfixadmin hat gar keine
entsprechende Einstellung. Roundcubes force_https bleibt davon unberuehrt
und muss weiterhin aus bleiben, sonst entstehen Redirect-Schleifen.
Dem Header darf hier getraut werden: Port 80 ist nur exposed und nie
published, der Container haengt im internen reverse-proxy-Netz und ist
damit ausschliesslich ueber den Proxy erreichbar. Die Ableitung bleibt
zudem bedingt -- eine Anfrage, die tatsaechlich ueber HTTP kommt, wird
nicht als sicher ausgegeben.
Die fastcgi_param-Zeile steht bewusst nach dem Include: fastcgi.conf
setzt HTTPS bereits aus $https, was hier leer ist und wegen if_not_empty
nichts uebertraegt -- eine Kollision gibt es also nicht.
Gebaut und im Container geprueft:
- nginx -t bestaetigt die Konfiguration, der map-Block auf http-Ebene
wird akzeptiert
- ohne Header ist $_SERVER['HTTPS'] nicht gesetzt
- mit X-Forwarded-Proto: https steht dort "on"
- mit X-Forwarded-Proto: http bleibt es ungesetzt
- Postfixadmins Warnung "connection not secure" erscheint ohne Header und
verschwindet mit ihm
Bislang waren Postfaecher nur mit einem IMAP-Client erreichbar. Roundcube laeuft jetzt neben Postfixadmin im selben Container: nginx und php-fpm sind ohnehin da, und weil Postfixadmin mit md5crypt hasht und Dovecot auf MD5-CRYPT steht, authentifizieren sich Nutzer mit ihrer gewohnten Mailbox-Adresse und deren Passwort -- ohne zweiten Account. Das "complete"-Tarball bringt sein vendor-Verzeichnis mit, es wird also kein composer im Image gebraucht. Ausgeliefert wird nur public_html; config, temp, logs und vendor sind dessen Geschwister und damit von aussen konstruktiv unerreichbar. installer.php wird geloescht statt nur per enable_installer abgeschaltet. Versand laeuft bewusst ueber Submission auf 587 mit den Zugangsdaten des angemeldeten Nutzers, nicht ueber Port 25: Dort wuerde permit_mynetworks Mail aus 127.0.0.0/8 ohne Authentifizierung annehmen, und reject_authenticated_sender_login_mismatch greift nur bei authentifizierten Absendern -- ein Nutzer koennte dann als jedes andere Postfach dieses Servers senden. Submission verlangt STARTTLS; die Zertifikatspruefung ist fuer diese Verbindung abgeschaltet, weil das Zertifikat auf den Mailhostnamen lautet und wir localhost ansprechen. Das ist hier unbedenklich, da die Verbindung den Container nie verlaesst. Das Plugin identity_switch macht den eigentlichen Wunsch moeglich: mehrere Postfaecher in einer Sitzung, mit Kontenumschalter und Ungelesen-Zaehler. Es unterstuetzt nur den Elastic-Skin, der deshalb ausdruecklich gesetzt ist. Der des_key verschluesselt Sitzungsinhalte und die vom Plugin gespeicherten Passwoerter der Zweitkonten. Ist ROUNDCUBE_DES_KEY leer, erzeugt roundcube-init.sh einmalig einen Schluessel im Volume -- ein leerer Schluessel wuerde diese Passwoerter ohne sichtbaren Fehler unverschluesselt lassen. Derselbe Startschritt legt Schema, temp und logs an; er ist idempotent und wie die uebrigen Schritte mit "-" versehen, damit ein Problem des Webmailers niemals die Mailzustellung am Starten hindert. In der nginx-Konfiguration ist zweierlei wesentlich: "^~" laesst den Webmail-Pfad vor der .php-Regex gewinnen, die sonst Roundcubes Skripte mit Postfixadmins Docroot ausfuehren wuerde. Und es gibt bewusst kein try_files -- innerhalb einer alias-Location loest es $uri falsch auf, wodurch unbekannte Pfade bei Postfixadmin landeten statt 404 zu liefern. Roundcube braucht keinen Front-Controller-Fallback: jede Anfrage ist eine echte Datei oder ein Asset, das static.php ueber PATH_INFO ausliefert. Nicht enthalten: serverseitige Filter und Abwesenheitsnotizen. Das Image fuehrt weder sieve noch managesieve, deshalb ist Roundcubes managesieve-Plugin aus. Gebaut und im Container geprueft, zuletzt gegen einen Build von null: - alle sechs Dienste erreichen RUNNING, nginx -t ist zufrieden - /webmail/ und /webmail/index.php liefern 200 mit der Loginseite - installer.php, config/, logs/ und vendor/ liefern 404 - Assets ueber static.php und der favicon-Rewrite liefern 200 - Postfixadmin unter / funktioniert unveraendert weiter - das Session-Cookie traegt secure und HttpOnly, sobald X-Forwarded-Proto: https anliegt - echter Login mit einem Postfach aus der Postfixadmin-Datenbank gelingt, Dovecot protokolliert die IMAP-Sitzung, und identity_switch ist in der gerenderten Seite nachweisbar geladen - Schema samt der identity_switch-Spalte in identities wird angelegt - Submission auf 587: STARTTLS-Handshake und AUTH PLAIN gelingen, und ein fremder Absender wird bei RCPT TO mit "553 5.7.1 Sender address rejected: not owned by user" abgewiesen
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
No description provided.