Fetchmail can't deliver messages to postfix [k8s/helm-chart]
📝 Preliminary Checks
- [x] I tried searching for an existing issue and followed the debugging docs advice, but still need assistance.
👀 What Happened?
Using the community helm chart. Enabled fetchmail and added one account/config to fetchmail.rc
Fetchmail starts, fetchesmMail but then fails to deliver it due to:
Helo command rejected: need fully-qualified hostname
My fetchmail is simply
poll xxxx with service imaps and timeout 120 and options no dns auth password
user "user" there with password "pass" is "existinguser@withuserdomain" here options fetchall ssl nokeep
👟 Reproduction Steps
No response
🐋 DMS Version
15.0.2
💻 Operating System and Architecture
k8s community Helm chart
⚙️ Container configuration files
📜 Relevant log output
2025-05-24T20:23:32.531389+00:00 docker-mailserver-5dfb984fb8-nkvdk fetchmail[67184]: reading message xxxx@xxxx:2 of 2 (7674 header octets) (log message incomplete)
2025-05-24T20:23:32.531398+00:00 docker-mailserver-5dfb984fb8-nkvdk fetchmail[67184]: SMTP error: 504 5.5.2 <docker-mailserver-5dfb984fb8-nkvdk>: Helo command rejected: need fully-qualified hostname
2025-05-24T20:23:32.531905+00:00 docker-mailserver-5dfb984fb8-nkvdk postfix/postscreen[84741]: CONNECT from [::1]:38698 to [::1]:25
2025-05-24T20:23:32.531967+00:00 docker-mailserver-5dfb984fb8-nkvdk postfix/postscreen[84741]: PASS OLD [::1]:38698
Not a solution to your problem, but you might give getmail a try.
fails to deliver it due to:
Helo command rejected: need fully-qualified hostname
This is an error from Postfix and this will be the first result on Google for keywords fetchmail "need fully-qualified hostname".
You could relax security on the Postfix side (via our Postfix override support), or consider why fetchmail isn't providing an acceptable HELO/EHLO.
Alternatively, this mailing list discussion suggests that fetchmail (or possibly Postfix too IIRC) would attempt rDNS on the IP of the network interface being used to connect through, or whatever is configured as the hostname on the system.
As you've mentioned you're using kubernetes, it lacks support for setting an explicit hostname from what I've heard through @georglauterbach , so you need to set our ENV OVERRIDE_HOSTNAME. I'm not familiar with our associated Helm chart project that well, I'd assume it has something in place to handle that?
Without that ENV being set, you'll have a single DNS label as the hostname by default AFAIK, containers usually have that set as a random hexadecimal ID. That is not a fully-qualified hostname AFAIK? (it'd expect a 2nd label I think?)
The alternative is like @casperklein suggested, to prefer Getmail instead. Fetchmail will deliver through SMTP it seems while Getmail differs by configuring the destination to run a command instead, which would avoid the issue entirely.
Do note that the two services behave slightly different beyond that. Our Getmail is not the latest release as we're using Debian packages for it, there are some features that newer Getmail added that it had lacked compared to Fetchmail, one was regarding the read/seen status change to remote IMAP inbox IIRC (if you didn't want to delete). Ensure that once it's setup it functions the way you expect 👍
OVERRIDE_HOSTNAME was already set, the other things you mentioned are clear.
IMHO rDNS is not the issue here as fetchmail simply uses localhost in the HELO. Not going to further debug this though.
Also not going to relax standard postfix settings as they are there for a reason, so I switched to getmail as suggested.
My expectation is that there is at least a note in the docs that fetchmail does not work out of the box under k8s.
IMHO rDNS is not the issue here as fetchmail simply uses localhost in the HELO.
localhost is not a FQDN, mail.localhost would be though.
I switched to getmail as suggested.
👍
My expectation is that there is at least a note in the docs that fetchmail does not work out of the box under k8s.
Kubernetes support is unofficial, it is community managed (hence separate repo, and clarification in our docs).
You are welcome to PR that note to the fetmail docs page, it's just markdown so quite simple to contribute.
It should work though, provided the hostname is resolved to an FQDN. However fetchmail is a community contributed feature as well, no maintainer is that familiar with it's config and support, so if it lacks the ability to override the hostname, then yes without kubernetes supporting a way to configure the hostname of the container, it's not going to work I guess 😅
I moved this issue to our K8s repository.
My expectation is [...]
My expectation is that those who open issues have understood that this project is maintained by individuals in their spare time. If there is an issue, you ask for help, and we'll do our best to resolve the issue.
To the matter at hand.
@cfis are you using fetchmail accidentally, or do you know anything about it not working? If so, we should (as was said) add a comment to the docs somewhere.
I am not using fetchmail - so don't know if it works or not. Based on this ticket though - https://github.com/docker-mailserver/docker-mailserver/issues/990 - it seems to have been working on Kubernetes in 2018 so seems like it should work?
As for OVERRIDE_HOSTNAME - you MUST set it. See https://github.com/docker-mailserver/docker-mailserver-helm/blob/master/charts/docker-mailserver/values.yaml#L63.
@Xnyle how do you want to proceed on this issue? Sounds like you have moved to GetMail so should I close this? Or if you want to debug further that would be great also.
For comparison to the 2018 referenced fetchmail config:
This issue:
poll xxxx with service imaps
and timeout 120
and options no dns auth password
user "user" there
with password "pass"
is "existinguser@withuserdomain" here
options fetchall ssl nokeep
2018 issue:
poll 'imap.mail.eu-west-1.awsapps.com' proto IMAP
user '[email protected]'
password 'PPP'
is 'XXX'
ssl
There has been many changes since 2018 though, chances are Postfix wasn't as strict on HELO.
EDIT: Postfix config had the config for HELO restriction added in April 2016 into the DMS v2 branch, so presumably it applied to the 2018 issue.
I don't have time to investigate atm, but perhaps it's specific to the fetchmail config differences only? (or possibly container runtimes back then were slightly different, I do recall we also treated hostname config quite differently too)
For reference, this is what our docs advise instead:
So it's unclear where @Xnyle came up with their Fetchmail config, unless they've used fetchmail elsewhere before (presumably in k8s environment too).
We also have a compose.yaml fetchmail example reference for testing, could probably not set the hostname and rely only on the OVERRIDE_HOSTNAME ENV to reproduce there.
Just adjust that config and run the commands detailed here should be sufficient, otherwise might need to be adapted to k8s if that works fine with compose.
Yes, my fetchmail config existed b4 outside of any container config.
BUT I already ensured that OVERRIDE_HOSTNAME is set and also even points to the correct Service IP. And I also used the very config (ofc user changed) from the docs.
But that didn't change anything.
I also tried to find a setting in fetchmail that steers the HELO "banner" is sends but couldn't find any that has any change.
I doubt this would resolve the fetchmail concern, but it is possible to replace the hostname binary with a shell script that emits the hostname you'd prefer instead. We needed that at build when installing Postfix, so you can reference what we did.
Presumably a service would instead use a syscall or similar to check the hostname, in which case that won't work.
I don't know k8s well, but this 2024 comment from a k8s maintainer states Pods have a hostname field that can be configured if that's helpful. Someone also shares a shell script attempt to change the hostname in other locations that might be checked.
Not sure I'f it's worth investigaring / solving as 90% of what you can do with fetchmail you can also do with getmail. Fetchmail would only be needed if you absolutely need to get the mail through postfix again in order to spam check/redeliver it to some other server?
This use case might be so much out of the scope of a all in one self hosted container solution? But there should be a note (a big bold one) in the docs that fetchmail+k8s is not going to work ATM.
NOTE: Nobody needs to read this comment and my next follow-up comment, they're verbose for reference purposes.
Reproducible with Docker Compose. There appears to be no ability to fake the hostname for that service without setting hostname in compose.yaml:
services:
# Your DMS container with fetchmail enabled:
dms-fetch:
image: ghcr.io/docker-mailserver/docker-mailserver:latest # :15.0
# Disabled for OVERRIDE_HOSTNAME reproduction:
#hostname: mail.example.test
environment:
OVERRIDE_HOSTNAME: mail.example.test
ENABLE_FETCHMAIL: 1
# We change this setting to 10 for quicker testing:
FETCHMAIL_POLL: 10
# Additionally try to override the hostname this way:
HOSTNAME: hello-dms.test
# You'd normally use `volumes` here but for simplicity of the example, all config is contained within `compose.yaml`:
configs:
- source: dms-accounts-fetch
target: /tmp/docker-mailserver/postfix-accounts.cf
- source: fetchmail
target: /tmp/docker-mailserver/fetchmail.cf
# Use user-patches.sh to attempt configuring additional fake hostnames:
- source: try-fake-hostnames
target: /tmp/docker-mailserver/user-patches.sh
# This is only for this example, since no real DNS service is configured, this is a Docker internal DNS feature:
networks:
default:
aliases:
- mail.example.test
# This is needed to resolve `@example.test` as a postfix security check on the sender address:
- example.test
# This container represents your remote IMAP service that receives mail that fetchmail pulls from:
dms-remote:
image: ghcr.io/docker-mailserver/docker-mailserver:latest # :15.0
#hostname: mail.remote.test
environment:
OVERRIDE_HOSTNAME: mail.remote.test
# Allows for us send a test mail easily by trusting any mail client run within this container (`swaks`):
PERMIT_DOCKER: container
configs:
- source: dms-accounts-remote
target: /tmp/docker-mailserver/postfix-accounts.cf
# Private DNS entries to resolve these queries to this container in the docker network:
networks:
default:
aliases:
- mail.remote.test
# This would be needed if you don't specify `--server` to swaks, resolving the `--to` address for which MTA to deliver to:
- remote.test
# Using the Docker Compose `configs.content` feature instead of volume mounting separate files.
# NOTE: This feature requires Docker Compose v2.23.1 (Nov 2023) or newer:
# https://github.com/compose-spec/compose-spec/pull/446
configs:
fetchmail:
content: |
poll 'mail.remote.test' proto imap
user '[email protected]'
pass 'secret'
is '[email protected]'
no sslcertck
# DMS requires an account to complete setup, configure one for each instance:
# NOTE: Both accounts are configured with the same password (SHA512-CRYPT hashed), `secret`.
dms-accounts-fetch:
content: |
[email protected]|{SHA512-CRYPT}$$6$$sbgFRCmQ.KWS5ryb$$EsWrlYosiadgdUOxCBHY0DQ3qFbeudDhNMqHs6jZt.8gmxUwiLVy738knqkHD4zj4amkb296HFqQ3yDq4UXt8.
dms-accounts-remote:
content: |
[email protected]|{SHA512-CRYPT}$$6$$sbgFRCmQ.KWS5ryb$$EsWrlYosiadgdUOxCBHY0DQ3qFbeudDhNMqHs6jZt.8gmxUwiLVy738knqkHD4zj4amkb296HFqQ3yDq4UXt8.
# A `user-patches.sh` script:
try-fake-hostnames:
content: |
#!/bin/bash
echo 'hello.test' > /etc/hostname
echo 'world.test' > /etc/mailname
# Replace the `hostname` command:
mv /usr/bin/hostname /usr/bin/hostname.bak
echo -e "#!/bin/bash\necho 'docker-mailserver.invalid'" >/usr/bin/hostname
chmod +x /usr/bin/hostname
# This will fail as `/etc/hosts` is an implicit bind mount, but you can edit it manually:
# NOTE: The extra `$` is because this script is embedded into `compose.yaml`, `$$` is required to escape `$`.
sed -i "s|$${HOSTNAME}|hello-world.test|" /etc/hosts
Sending mail
Send a mail from the swaks CLI within the dms-remote container for dms-fetch to later retrieve:
$ docker compose exec -it dms-remote swaks --server localhost --from [email protected] --to [email protected] --silent
<** 504 5.5.2 <42365d7ec29b>: Helo command rejected: need fully-qualified hostname
-> QUIT
<- 221 2.0.0 Bye
=== Connection closed with remote host.
# Related failure logs:
$ docker compose logs dms-remote | grep '[email protected]'
policyd-spf[793]: : prepend X-Comment: SPF check N/A for local connections - client-ip=::1; helo=058cb200d2bd; [email protected]; receiver=remote.test
postfix/smtpd[789]: NOQUEUE: reject: RCPT from localhost[::1]: 504 5.5.2 <058cb200d2bd>: Helo command rejected: need fully-qualified hostname; from=<[email protected]> to=<[email protected]> proto=ESMTP helo=<058cb200d2bd>
Like fetchmail, swaks will default the helo to the detected hostname of the container it's running in.
That is a similar error to fetchmail.. except swaks can fix it by changing the helo value to use via the --helo option:
# Append a `--helo`:
$ docker compose exec -it dms-remote swaks --server localhost --from [email protected] --to [email protected] --silent --helo mail.example.test
# Mail was not rejected this time:
$ docker compose logs dms-remote | grep '[email protected]'
policyd-spf[807]: : prepend X-Comment: SPF check N/A for local connections - client-ip=::1; helo=mail.example.test; [email protected]; receiver=remote.test
postfix/qmgr[711]: 401EFE5C7C: from=<[email protected]>, size=759, nrcpt=1 (queue active)
amavis[764]: (00764-01) Passed CLEAN {RelayedInbound}, [::1]:56796 <[email protected]> -> <[email protected]>, Queue-ID: 14F89E5C74, Message-ID: <20250527073909.000927@42365d7ec29b>, mail_id: 6pkDqfMRJSbO, Hits: -, size: 549, queued_as: 401EFE5C7C, 118 ms
# Related logs for successful receive + store:
$ docker compose logs dms-remote | grep '[email protected]'
postfix/smtp-amavis/smtp[937]: 14F89E5C74: to=<[email protected]>, relay=127.0.0.1[127.0.0.1]:10024, delay=0.2, delays=0.07/0.01/0.01/0.12, dsn=2.0.0, status=sent (250 2.0.0 from MTA(smtp:[127.0.0.1]:10025): 250 2.0.0 Ok: queued as 401EFE5C7C)
dovecot: lmtp([email protected])<940><l+iCEB1sNWisAwAA2SGYFA>: sieve: msgid=<20250527073909.000927@42365d7ec29b>: stored mail into mailbox 'INBOX'
postfix/lmtp[766]: 401EFE5C7C: to=<[email protected]>, relay=mail.remote.test[/var/run/dovecot/lmtp], delay=0.03, delays=0/0/0.01/0.01, dsn=2.0.0, status=sent (250 2.0.0 <[email protected]> l+iCEB1sNWisAwAA2SGYFA Saved)
Successful delivery to dms-remote, great, but fetchmail itself will still fail with only OVERRIDE_HOSTNAME instead of hostname. None of the attempts to fake the hostname worked.
It looks like env.c in the Fetchmail source is where this is checked via glibc call via their host_fqdn() function.
Thus it'll be via a kernel syscall I think?:
- https://linux.die.net/man/2/gethostname
cat /proc/sys/kernel/hostnameoruname -nboth return the hostname configured in the container.- As the
/proc/sys/..prefix would imply, you can query that viasysctl kernel.hostnametoo. Container runtime support for modifying such settings varies I think. Docker is rather limited (1 + 2),kernel.hostnameis not one that is permitted to be set, even though it reflects the hostname of the namespaced container?
Will need to explore some alternative solutions..
Side-note
I'm not sure what to think about AI driven solutions ingesting our project and the considerable time I spend to thoroughly document/troubleshoot concerns like this publicly for the benefits of others 😅 (I do this a fair amount, not just the DMS repos)
Our OVERRIDE_HOSTNAME ENV has been interpreted as more generic for Docker / containers, rather than specific to DMS:
On the bright-side, at least the AI response is providing citations by linking to source material at least.
3 workarounds for fetchmail when relying on the OVERRIDE_HOSTNAME ENV
TL;DR:
PERMIT_DOCKER=containeras a quick fix, should be reasonably safe to use.fetchmail.cfconfig options:--smtphostto deliver direct to LMTP (via unix socket) since the fetchmail service in DMS is bundled into the same container as Dovecot.--mdato delegate to a command for deliver instead of via SMTP. Likewise suitable for local usage, added advantage that we can filter through one of our anti-spam services before delivering to Dovecot.
Presently --mda will be a hassle, DMS will need to have someone contribute changes to make it more viable. PERMIT_DOCKER=container is the least amount of friction to go with if you are ok with trusting any process in the container.
Or as suggested earlier, just go with Getmail 😎
Relax security via PERMIT_DOCKER=container
The broader stroke "fix" is to lean into our PERMIT_DOCKER=container feature.
- Any local connection within the container would then be trusted, skipping security checks (the sender address having valid DNS wouldn't be required either, so the network alias
example.testassigned to thedms-fetchcontainer could be dropped too). - In fact, if you set that ENV on the
dms-fetchcontainer, it'll likewise workaround the problem you encountered with the HELO check. Security wise, it might be acceptable, but I'd encourage considering the other workarounds I've documented here.
With PERMIT_DOCKER=container set on both DMS containers (add this to the previous compose.yaml snippet):
# Add PERMIT_DOCKER=container env:
services:
dms-fetch:
environment:
# Allows for us send a test mail easily by trusting any mail client run within this container (`fetchmail`):
PERMIT_DOCKER: container
dms-remote:
environment:
# Allows for us send a test mail easily by trusting any mail client run within this container (`swaks`):
PERMIT_DOCKER: container
$ docker compose up -d --force-recreate
$ docker compose exec -it dms-remote swaks --server localhost --from [email protected] --to [email protected] --silent
# dms-remote has the mail stored until fetchmail retrieves it for dms-fetch:
$ docker compose exec -it dms-remote doveadm mailbox status -u [email protected] messages INBOX
INBOX messages=1
$ docker compose exec -it dms-fetch doveadm mailbox status -u [email protected] messages INBOX
INBOX messages=0
# Once fetchmail is triggered at the polling interval (success, no helo failure):
$ docker compose exec -it dms-remote doveadm mailbox status -u [email protected] messages INBOX
INBOX messages=0
$ docker compose exec -it dms-fetch doveadm mailbox status -u [email protected] messages INBOX
INBOX messages=1
LMTP delivery (deliver direct to Dovecot instead of going through Postfix first)
Using the fetchmail config option smtphost / --smtphost (CLI option), we can give a regular SMTP hostname or a unix socket instead for LMTP. When this setting isn't configured, the default is localhost.
Preferring to deliver via LMTP should technically be ok - in fact going through SMTP is typically discouraged for fetchmail/getmail when they're delivering mail they've retrieved. Postfix internally will hand the mail off to Dovecot via LMTP.
NOTE: This will skip all security checks that Postfix would have normally done. That should be acceptable, given that we're trusting the internal fetchmail service itself, however it also means any anti-spam/virus checks are skipped too.
To do this, we just add to the fetchmailrc configuration (fetchmail.cf) this extra setting:
smtphost /var/run/dovecot/lmtp
That unix socket is defined in /etc/dovecot/conf.d/10-master.conf, where we have ownership and group of docker / 5000. Thus to avoid an error that will occur, the fetchmail user must also exist in the docker group, which we'll workaround for now via a user-patches.sh script (fix-fetchmail):
services:
dms-fetch:
image: ghcr.io/docker-mailserver/docker-mailserver:latest # :15.0
#hostname: mail.example.test
environment:
OVERRIDE_HOSTNAME: mail.example.test
ENABLE_FETCHMAIL: 1
FETCHMAIL_POLL: 10
configs:
- source: dms-accounts-fetch
target: /tmp/docker-mailserver/postfix-accounts.cf
- source: fetchmail
target: /tmp/docker-mailserver/fetchmail.cf
# Add fetchmail to the postfix group for Dovecot LMTP socket access:
- source: fix-fetchmail
target: /tmp/docker-mailserver/user-patches.sh
networks:
default:
aliases:
- mail.example.test
- example.test
dms-remote:
image: ghcr.io/docker-mailserver/docker-mailserver:latest # :15.0
#hostname: mail.remote.test
environment:
OVERRIDE_HOSTNAME: mail.remote.test
# Optional - Avoiding the need to override the HELO when using `swaks` CLI:
PERMIT_DOCKER: container
configs:
- source: dms-accounts-remote
target: /tmp/docker-mailserver/postfix-accounts.cf
# Private DNS resolve these queries to this container in the docker network:
networks:
default:
aliases:
- mail.remote.test
- remote.test
# Using the Docker Compose `configs.content` feature instead of volume mounting separate files.
# NOTE: This feature requires Docker Compose v2.23.1 (Nov 2023) or newer:
# https://github.com/compose-spec/compose-spec/pull/446
configs:
fetchmail:
content: |
poll 'mail.remote.test' proto imap
user '[email protected]'
pass 'secret'
is '[email protected]'
no sslcertck
smtphost /var/run/dovecot/lmtp
# DMS requires an account to complete setup, configure one for each instance:
# NOTE: Both accounts are configured with the same password (SHA512-CRYPT hashed), `secret`.
dms-accounts-fetch:
content: |
[email protected]|{SHA512-CRYPT}$$6$$sbgFRCmQ.KWS5ryb$$EsWrlYosiadgdUOxCBHY0DQ3qFbeudDhNMqHs6jZt.8gmxUwiLVy738knqkHD4zj4amkb296HFqQ3yDq4UXt8.
dms-accounts-remote:
content: |
[email protected]|{SHA512-CRYPT}$$6$$sbgFRCmQ.KWS5ryb$$EsWrlYosiadgdUOxCBHY0DQ3qFbeudDhNMqHs6jZt.8gmxUwiLVy738knqkHD4zj4amkb296HFqQ3yDq4UXt8.
fix-fetchmail:
content: |
#!/bin/bash
adduser fetchmail postfix
So that works, and removed the need for a PERMIT_DOCKER=container on our dms-fetch container 👍
$ docker compose up -d --force-recreate
$ docker compose exec -it dms-remote swaks --server localhost --from [email protected] --to [email protected] --silent
# dms-remote has the mail stored until fetchmail retrieves it for dms-fetch:
$ docker compose exec -it dms-remote doveadm mailbox status -u [email protected] messages INBOX
INBOX messages=1
$ docker compose exec -it dms-fetch doveadm mailbox status -u [email protected] messages INBOX
INBOX messages=0
# Once fetchmail is triggered at the polling interval (success, no helo failure):
$ docker compose exec -it dms-remote doveadm mailbox status -u [email protected] messages INBOX
INBOX messages=0
$ docker compose exec -it dms-fetch doveadm mailbox status -u [email protected] messages INBOX
INBOX messages=1
If you need to have a bit more flexibility, including spam checking, then the alternative fetchmail config setting --mda might be better.
Fetchmail config option --mda
--mda (or mda in fetchmailrc config) will allow you to deliver mail by delegating to a command to run instead. Useful if you want to also delegate via spamc or rspamc commands for spam filtering before handing to Dovecot for storing.
Config is the same as the LMTP example, except we replace smtphost /var/run/dovecot/lmtp for mda "/usr/lib/dovecot/deliver -d %T" where fetchmail will change %T to the recipient address. This command is roughly the same as delivering via LMTP AFAIK, just the CLI equivalent? There's also no need for the user-patches.sh script.
Compose config example
services:
dms-fetch:
image: ghcr.io/docker-mailserver/docker-mailserver:latest # :15.0
#hostname: mail.example.test
environment:
OVERRIDE_HOSTNAME: mail.example.test
ENABLE_FETCHMAIL: 1
FETCHMAIL_POLL: 10
configs:
- source: dms-accounts-fetch
target: /tmp/docker-mailserver/postfix-accounts.cf
- source: fetchmail
target: /tmp/docker-mailserver/fetchmail.cf
networks:
default:
aliases:
- mail.example.test
- example.test
dms-remote:
image: ghcr.io/docker-mailserver/docker-mailserver:latest # :15.0
#hostname: mail.remote.test
environment:
OVERRIDE_HOSTNAME: mail.remote.test
# Optional - Avoiding the need to override the HELO when using `swaks` CLI:
PERMIT_DOCKER: container
configs:
- source: dms-accounts-remote
target: /tmp/docker-mailserver/postfix-accounts.cf
# Private DNS resolve these queries to this container in the docker network:
networks:
default:
aliases:
- mail.remote.test
- remote.test
# Using the Docker Compose `configs.content` feature instead of volume mounting separate files.
# NOTE: This feature requires Docker Compose v2.23.1 (Nov 2023) or newer:
# https://github.com/compose-spec/compose-spec/pull/446
configs:
fetchmail:
content: |
poll 'mail.remote.test' proto imap
user '[email protected]'
pass 'secret'
is '[email protected]'
no sslcertck
mda "/usr/lib/dovecot/deliver -d %T"
# DMS requires an account to complete setup, configure one for each instance:
# NOTE: Both accounts are configured with the same password (SHA512-CRYPT hashed), `secret`.
dms-accounts-fetch:
content: |
[email protected]|{SHA512-CRYPT}$$6$$sbgFRCmQ.KWS5ryb$$EsWrlYosiadgdUOxCBHY0DQ3qFbeudDhNMqHs6jZt.8gmxUwiLVy738knqkHD4zj4amkb296HFqQ3yDq4UXt8.
dms-accounts-remote:
content: |
[email protected]|{SHA512-CRYPT}$$6$$sbgFRCmQ.KWS5ryb$$EsWrlYosiadgdUOxCBHY0DQ3qFbeudDhNMqHs6jZt.8gmxUwiLVy738knqkHD4zj4amkb296HFqQ3yDq4UXt8.
I don't think I've updated the Getmail docs yet with a WIP revision I had which demonstrates using rspamc / spamc for filtering spam, but it'd be roughly the same for fetchmail with --mda.
Full walkthrough (verbose, click to view)
Unlike the LMTP approach, this has quite a few hoops to jump through to get into a workin state. Documenting for reference.
$ docker compose up -d --force-recreate
# Send a mail to dms-remote for dms-fetch to retrieve and store in it's container:
docker compose exec -it dms-remote swaks --server localhost --from [email protected] --to [email protected]
# Once fetchmail polls 10s later, check the logs and we'll see error output:
docker compose logs dms-fetch
fetchmail[679]: 1 message for [email protected] at mail.remote.test.
dovecot: auth: Error: client doesn't have lookup permissions for this user: userdb uid (5000) doesn't match peer uid (101) (to bypass this check, set: service auth { unix_listener /run/dovecot/auth-userdb { mode=0777 } })
dovecot: lda([email protected])<797><>: Error: auth-master: userdb lookup([email protected]): Auth USER lookup failed
dovecot: lda(797): Fatal: Internal error occurred. Refer to server log for more information.
fetchmail[679]: reading message [email protected]@mail.remote.test:1 of 1 (813 header octets) (28 body octets) (log message incomplete)
fetchmail[679]: MDA returned nonzero status 75
fetchmail[679]: not flushed
/etc/dovecot/conf.d/10-master.conf must be edited to relax permissions (update mode):
service auth {
# auth_socket_path points to this userdb socket by default. It's typically
# used by dovecot-lda, doveadm, possibly imap process, etc. Users that have
# full permissions to this socket are able to get a list of all usernames and
# get the results of everyone's userdb lookups.
#
# The default 0666 mode allows anyone to connect to the socket, but the
# userdb lookups will succeed only if the userdb returns an "uid" field that
# matches the caller process's UID. Also if caller's uid or gid matches the
# socket's uid or gid the lookup succeeds. Anything else causes a failure.
#
# To give the caller full permissions to lookup all users, set the mode to
# something else than 0666 and Dovecot lets the kernel enforce the
# permissions (e.g. 0777 allows everyone full permissions).
unix_listener auth-userdb {
mode = 0777
user = docker
group = docker
}
Then we can run dovecot reload to have Dovecot respond to the update:
$ dovecot reload
# Confirm the changes are applied:
$ stat -c '%a' /var/run/dovecot/auth-userdb
777
Now we get the following failure about the group nogroup / 65534 not matching what was expected:
fetchmail[1884]: 1 message for [email protected] at mail.remote.test.
dovecot: lda([email protected])<1903><x1QWMuoONWhvBwAAEAQt/A>: Fatal: setgid(5000(docker) from userdb lookup) failed with euid=101(fetchmail), gid=65534(nogroup), egid=65534(nogroup): Operation not permitted (This binary should probably be called with process group set to 5000(docker) instead of 65534(nogroup))
fetchmail[1884]: reading message [email protected]@mail.remote.test:1 of 1 (813 header octets) (28 body octets) (log message incomplete)
fetchmail[1884]: MDA returned nonzero status 75
fetchmail[1884]: not flushed
We can resolve that:
# Replace the primary group of the `fetchmail` user to now be `docker` (5000):
# NOTE: `adduser docker fetchmail` would not work as the primary group is what needs to be matched
usermod -g docker fetchmail
# Restart the service so the fetchmail process now runs with the updated primary group:
supervisorctl restart fetchmail
Instead of a setgid error, we now get the same for setuid:
# ...
Fatal: setuid(5000(docker) from userdb lookup) failed with euid=101(fetchmail): Operation not permitted (This binary should probably be called with process user set to 5000(docker) instead of 101(fetchmail))
Dovecot is using setuid + setgid to switch based on /etc/dovecot/userdb entry (which has 5000:5000 set, aka docker:docker for UID/GID):
[email protected]:{SHA512-CRYPT}$6$sbgFRCmQ.KWS5ryb$EsWrlYosiadgdUOxCBHY0DQ3qFbeudDhNMqHs6jZt.8gmxUwiLVy738knqkHD4zj4amkb296HFqQ3yDq4UXt8.:5000:5000::/var/mail/example.test/john.doe/home::
So fetchmail service would need to run as docker:docker (the Dovecot vmail user/group in DMS). Assigning the supervisord service a user of docker would then use the docker primary group, but that'll fail, but so will running as root due to an owner mismatch for the config at /etc/failmailrc:
# Running fetchmail as root:
$ USER=fetchmail HOME="/var/lib/fetchmail" /usr/bin/fetchmail -f /etc/fetchmailrc --nodetach --nosyslog
fetchmail: WARNING: Running as root is discouraged.
File /etc/fetchmailrc must be owned by you.
# Fix and try again:
$ chown root /etc/fetchmailrc
$ USER=fetchmail HOME="/var/lib/fetchmail" /usr/bin/fetchmail -f /etc/fetchmailrc --nodetach --nosyslog
fetchmail: WARNING: Running as root is discouraged.
fetchmail: starting fetchmail 6.4.37 daemon
1 message for [email protected] at mail.remote.test.
reading message [email protected]@mail.remote.test:1 of 1 (813 header octets) (28 body octets) flushed
fetchmail: sleeping at Tue May 27 04:09:30 2025 for 300 seconds
$ chown docker /etc/fetchmailrc
# Run a process with different UID/GID, similar to what supervisor would do:
$ setpriv --reuid=docker --regid=docker --clear-groups -- bash -c 'USER=fetchmail HOME="/var/lib/fetchmail" /usr/bin/fetchmail -f /etc/fetchmailrc --nodetach --nosyslog'
fetchmail: lstat: /var/lib/fetchmail/.fetchids: Permission denied
# Fix that too, try again and it should work now:
$ chown docker /var/lib/fetchmail
$ setpriv --reuid=docker --regid=docker --clear-groups -- bash -c 'USER=fetchmail HOME="/var/lib/fetchmail" /usr/bin/fetchmail -f /etc/fetchmailrc --nodetach --nosyslog'
fetchmail: starting fetchmail 6.4.37 daemon
1 message for [email protected] at mail.remote.test.
reading message [email protected]@mail.remote.test:1 of 1 (813 header octets) (28 body octets) flushed
fetchmail: sleeping at Tue May 27 04:14:42 2025 for 300 seconds
# Change fetchmail service `user` to `docker`:
nano /etc/supervisor/conf.d/dms-services.conf
# Refresh the service config of supervisord so it runs the updated config:
supervisorctl update fetchmail
supervisorctl start fetchmail
One more permission issue to address:
fetchmail[9148]: fetchmail: lock creation failed, pidfile "/var/run/fetchmail/fetchmail.pid": Permission denied
dms-fetch-1 | 2025-05-27 04:18:53,253 WARN exited: fetchmail (exit status 8; not expected)
chown docker /var/run/fetchmail
Anyway... bulk of that can be skipped with a PR fix to DMS.
Reference
DMS container config files related content:
/etc/passwd:
fetchmail:x:101:65534::/var/lib/fetchmail:/bin/false
/etc/group:
nogroup:x:65534:
Our supervisord config dms-services.conf has these two service configs (/etc/supervisor/conf.d/dms-services.conf in the container):
Supervisor does support user switching via a user setting, but not one for changing the group/GID for the process/service:
As such the only way that's really going to work is for fetchmail to run as our vmail user/group (presently assigned as docker). Or like the referenced service Getmail that is similar in functionality, running it as root to leverage setuid/setgid.
From the official fetchmail docs for the --mda command:
If fetchmail is running as root, it sets its user id while delivering mail through an MDA as follows:
- First, the
FETCHMAILUSER,LOGNAME, andUSERenvironment variables are checked in this order. The value of the first variable from his list that is defined (even if it is empty!) is looked up in the system user database. - If none of the variables is defined, fetchmail will use the real user id it was started with.
- If one of the variables was defined, but the user stated there is not found, fetchmail continues running as root, without checking remaining variables on the list.
Practically, this means that if you run fetchmail as root (not recommended), it is most useful to define the
FETCHMAILUSERenvironment variable to set the user that the MDA should run as.Some MDAs (such as maildrop) are designed to be
setuidroot andsetuidto the recipient's user id, so you do not lose functionality this way even when running fetchmail as unprivileged user. Check the MDA's manual for details.
Our supervisor config for fetchmail has the following which sets USER and HOME as part of the user switch:
environment=HOME="/var/lib/fetchmail",USER="fetchmail"
command=/usr/bin/fetchmail -f /etc/fetchmailrc --nodetach --daemon "%(ENV_FETCHMAIL_POLL)s" -i /var/lib/fetchmail/.fetchmail-UIDL-cache --pidfile /var/run/fetchmail/fetchmail.pid
Fetchmail can take args here, or as settings it's referenced fetchmailrc file (these are documented as Keyword values after the equivalent CLI option). Additional syntax for fetchmailrc is documented here. I need to update our docs at some point to reference these 🤔
How does PERMIT_DOCKER change the fact that postfix is configured to require a valid fqdn in the HELO? Or is that requirement not active for mynetworks? tilt
Bonus Question (answer could come handy for solving another problem with roundcube in a different container):
Use PERMIT_DOCKER=connected-networks in this case. connected-networks => Add all connected docker networks (ipv4 only).
Thats not going to work on k8s, right? How does the container even know what networks are "connected" it only sees his own veth?
How does
PERMIT_DOCKERchange the fact that postfix is configured to require a valid fqdn in the HELO? Or is that requirement not active for mynetworks? tilt
Being added to Postfix mynetworks setting will trust those clients, which skips a bunch of security checks:
- https://www.postfix.org/postconf.5.html#mynetworks
- https://github.com/docker-mailserver/docker-mailserver/blob/f28fce9cc432f1f447bd963d9e54e44bcf2c27dd/target/postfix/main.cf#L52-L62
The main.cf link shows various Postfix restrictions that begin with permit_mynetworks, that is what trusts any client that is from a network belonging to mynetworks. As the permit_mynetworks is the first check, if it's valid it skips the need to perform any of the security restrictions that would follow after it.
Bonus Question (answer could come handy for solving another problem with roundcube in a different container):
Use
PERMIT_DOCKER=connected-networksin this case.connected-networks=> Add all connected docker networks (ipv4 only).Thats not going to work on k8s, right? How does the container even know what networks are "connected" it only sees his own veth?
It's late here and as usual I'm short on time 😓
I've probably explained it this issue which documents flaws with the PERMIT_DOCKER ENV feature, as well as it's functionality.
You can inspect our shellscript for the check if you like. IIRC it queries the network interfaces that are of type veth (belongs to a bridged network) and applies a fixed CIDR mask to trust an entire subnet. As such it's making assumptions based on defaults with docker at the time it was contribtued that may not be correct in other environments/configurations.
Ok, assuming I got that right, the documentation on PERMIT_DOCKER is misleading:
host adds /16 of eth0 (veth) that's the whole cluster in k8s but NOT the host itself it' also way more than the connected-networks which just adds all visible networks which in k8s is just the pods /24
tldr; if you trust all containers in the host just go for "host" but don't expect the actual host to work ;-) connected-networks would work as well for fetchmal but not for other pods in the same namespace (e.g. roundcube) restricting to k8s NS only is probably impossible as there is no subnet abstraction between namespaces in flannel
Ok, assuming I got that right, the documentation on PERMIT_DOCKER is misleading
Yes, the issue I linked you to for PERMIT_DOCKER is all about how it's not implemented well.
I have a large backlog across OSS projects that I am working through, so most of my time in DMS is maintenance and troubleshooting like above. I haven't had as much time as I'd like to actually spin up PRs to resolve everything, but it does get added to the issue tracker should someone else find time to contribute before I do.
As such, apart from other concerns with PERMIT_DOCKER I would not recommend it. You can however observe what it is doing and apply your own modifications via our override config support or user-patches.sh, where you have it configured correctly instead.
Just be careful of trusting subnets which include the gateway if you deploy to an environment like Docker has been known to be problematic in the past (due to the default docker-proxy enabled) which routed IPv6 connections through the IPv4 only docker network via the gateway IP. That made DMS an open relay as any IPv6 client was implicitly trusted due to mynetworks trusting the gateway, I've said similar for PROXY protocol usage too.
tldr; if you trust all containers in the host just go for "host" but don't expect the actual host to work ;-) connected-networks would work as well for fetchmal but not for other pods in the same namespace (e.g. roundcube)
You'd prefer PERMIT_DOCKER=container if fetchmail only needs to deliver to the internal Dovecot service in the same container. If you need to forward mail externally, there is probably better alternatives.
As mentioned above, while it should be trust all containers, it's more to do with the IP the connection appears to be coming from and that won't always be the actual client if there's a misconfiguration. Testing is important.
I'm not familiar with your roundcube concern, but I am with kubernetes ingress needing to preserve the client IP from external traffic via PROXY protocol as that's what prompted me to revise our docs on the subject. Which gets a bit more complicated when your internal containers also need to connect to DMS directly without PROXY protocol (also documented).
Roundcube has some support to preserve client IP IIRC, which can be important as there is the risk of fail2ban (if enabled) rejecting traffic from roundcube for all it's users otherwise, if one of them fails logins to Dovecot enough that the logs from the same IP would trigger a ban.
restricting to k8s NS only is probably impossible as there is no subnet abstraction between namespaces in flannel
I'm not too familiar with networking in k8s, but I have heard it can be rather flexible and you can use a variety of network solutions/plugins? One user I believe had a script that would interact with their choice of networking software to get the IPs of relevant containers and update a file that DMS used for trust (Postfix mynetworks can point to a config file to source which IPs or subnets should be trusted, you might need to issue a postfix reload command after an update to refresh it in Postfix though, I think there is similar for Dovecot).
I've done another dive on this topic regarding the earlier concern noted with /etc/hosts.
It seems like we can fix the default behaviour when relying on OVERRIDE_HOSTNAME, I've confirmed the findings below work with fetchmail, the configured hostname via OVERRIDE_HOSTNAME ENV will be the HELO received by Postfix (or any other outbound connection relying on this approach to determine the hostname)
How software queries/resolves the container hostname (via libc, aka glibc/musl)
hostname --fqdn will query /proc/sys/kernel/hostname / sysctl kernel.hostname through glibc (relies on nsswitch.conf):
- If
nsswitch.conffirst queries thefilesresolver, then it will check if the hostname exists in/etc/hostsand return the first hostname from the matched line/entry associating the original queried hostname to an IP. - Another resolver in
/etc/nsswitch.confthat may follow as a fallback if no/etc/hostsmatch resolves, isdnswhich will perform a DNS query.
EDIT: This link provides an excellent overview of glibc calls getaddrinfo() + gethostbyname() (compatible with IPv4 entries only apparently, for both files and dns) behaviour with NSS and other related network config/interactions.
- EDIT:
hostname --fqdnseems to work fine with IPv6 entries in/etc/hosts?- Seems like it's due to linux adding a
gethostbyname2()variant? - Alpine by default with busybox
hostname -ffails with IPv6, but with thenet-toolspackage,hostname -fbecomes compatible with IPv6 too.
- Seems like it's due to linux adding a
- The
hostnamecommand as noted by that link is performing those calls (may differ a bit on musl?). - Also notes the source of error message upon failing to resolve,
No address associated with hostname.
HOSTNAME ENV always set in a container:
Additionally, within the container the HOSTNAME environment variable will always be set, even when we have a minimal image like this:
src/main.rs:
use std::env;
// This program will check the environment and print the key/value pairs found:
fn main() {
for (key, value) in std::env::vars() {
println!("{key}: {value}");
}
}
# Create basic rust project and modify `src/main.rs` to content shown above:
cargo init /tmp/example && cd /tmp/example
nano src/main.rs
# Build a static binary with `cargo build` / `cargo zigbuild`:
cargo zigbuild --release --target x86_64-unknown-linux-musl
cp ./target/x86_64-unknown-linux-musl/release/example ./check-env
Dockerfile:
FROM scratch
COPY env-check /env-check
ENTRYPOINT ["/env-check"]
# Create Dockerfile with content shown above, build the image and run:
$ nano Dockerfile
$ docker build --tag localhost/env-check .
$ docker run --rm -it --env EXAMPLE_ENV=42 localhost/env-check
PATH: /usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
HOSTNAME: fe9fd7d09328
TERM: xterm
EXAMPLE_ENV: 42
HOME: /
That image only has that basic program in it, no libc involved or anything else to set HOSTNAME, so that's being handled by Docker itself if --hostname is not used 😅
I assume kubernetes will also behave like that, despite lacking hostname configurability?
SYS_ADMIN capability
The hostname can also be set in a container if permissions are relaxed, but doing so requires the SYS_ADMIN capability to be granted, which is a bad idea.
$ docker run --rm -it --hostname hello.example.test debian:12-slim
$ hostname world.example.test
hostname: you must be root to change the host name
$ whoami
root
$ docker run --rm -it --hostname hello.example.test --cap-add SYS_ADMIN debian:12-slim
$ hostname
hello.example.test
$ hostname world.example.test
$ hostname
world.example.test
Updating the container /etc/hosts - Avoid changing the inode
The other concern we encountered was how to approach automating the update to /etc/hosts in the container since sed --in-place was incompatible:
$ docker run --rm -it debian:12-slim
# Match the container hostname in /etc/hosts and replace it with `example.test` (fails):
$ sed -i "s|${HOSTNAME}|example.test|" /etc/hosts
sed: cannot rename /etc/sedxigGKa: Device or resource busy
This is due to Docker implicitly bind mounting /etc/hosts when you start a container, just like any other volume with a bind mount it is tied to the inode. This is why you'd be better off avoiding bind mounts on specific files (if the inode changes on the host, your container will not receive that update as it still binds to the old file by original inode), while if you bind mount a directory you're free to change files within the container or host and the individual inodes there are not a concern, only the directory inode.
For reference you can check the inode of a file like this:
$ stat -c '%i' /etc/hosts
941270
We can however update a file without changing the inode too:
- via
cp - via piping stdin into
sponge(requires themoreutilspackage)
Here's an example demonstrating a change in hostname resolution to what we'd prefer instead for the canonical FQDN of our container:
$ docker run --rm -it debian:12-slim
$ hostname --fqdn
5f1d0fdb4de2
# Empty output as FQDN is single-label only:
$ hostname --domain
# Install the `sponge` command:
$ apt-get update -qq && apt-get install -qq moreutils
# Update the `/etc/hosts` entry with our desired hostname (OVERRIDE_HOSTNAME):
$ export OVERRIDE_HOSTNAME=mail.example.test
$ sed -E "s|($(hostname --fqdn))|${OVERRIDE_HOSTNAME} \1|" /etc/hosts | sponge /etc/hosts
# It now resolves the preferred hostname:
$ hostname --fqdn
mail.example.test
#
# Remaining commands are for reference purposes
#
# The full hostname (as per `cat /proc/sys/kernel/hostname`):
$ hostname
5f1d0fdb4de2
# Similar output to the default `hostname` command,
# If `sysctl kernel.hostname` has more than one DNS label/component,
# it will output only the first left-most DNS label (mail.example.test => mail)
$ hostname --short
5f1d0fdb4de2
# DNS domain name (truncates the first label, aka truncates `hostname --short`):
# - Effectively `dnsdomainname` / `domainname` command defaults?
# - Like `--fqdn` and `--alias` appears to rely on an NSS match being successful first.
$ hostname --domain
example.test
# All additional hostnames beyond `hostname --fqdn`, as defined for the `/etc/hosts` entry.
# - Resolved via matching `hostname --short` value to an IP.
# - Single line output with values delimited by white-space.
$ hostname --alias
5f1d0fdb4de2
# Not quite sure how this differs from `--fqdn`, I could not get it to output multiple FQDNs:
# UPDATE:
# - Appears to rely on matching /etc/hosts IP? (Did not require any matching hostname/FQDN, uses a DNS query?)
# - Doesn't seem relevant to `--fqdn`, outputs first hostname for that IP entry (FQDN are intended to be declared first)
$ hostname --all-fqdns
# This would fail when `--fqdn` + `--domainname` would too:
$ hostname --ip-address
172.17.0.2
# Reference content:
$ cat /etc/hosts
127.0.0.1 localhost
::1 localhost ip6-localhost ip6-loopback
fe00::0 ip6-localnet
ff00::0 ip6-mcastprefix
ff02::1 ip6-allnodes
ff02::2 ip6-allrouters
172.17.0.2 mail.example.test 5f1d0fdb4de2
NOTE: This hostname resolution behaviour may vary on other base image distros:
- Notably as I thoroughly documented previously in Oct 2021 the Alpine image has
hostnamecommand symlinked to BusyBox (/bin/busybox, only supportshostname -f, not long option--fqdn) where I noted a difference in behaviour unless thenet-toolspackage was installed which replaces the/bin/hostnamesymlink to an actual binary. - The ENV
HOSTNAMEsimilarly may be set in a manner that varies by base image (reference).
Reference + Docker CLI vs Docker Compose differences
For reference, when the container is configured with a custom hostname in Docker:
$ docker run --rm -it --hostname mail.example.test debian:12-slim
# Includes both FQDN (hostname --fqdn) and short hostname (hostname --short):
$ cat /etc/hosts
127.0.0.1 localhost
::1 localhost ip6-localhost ip6-loopback
fe00::0 ip6-localnet
ff00::0 ip6-mcastprefix
ff02::1 ip6-allnodes
ff02::2 ip6-allrouters
172.17.0.7 mail.example.test mail
$ hostname
mail.example.test
$ hostname --fqdn
mail.example.test
$ hostname --short
mail
$ hostname --domain
example.test
Additionally there is another difference between running a container with just the Docker CLI like shown above, and with Docker Compose.
- The Docker CLI continues to default the network to the legacy
docker0bridge, while Docker Compose defaults to creating custom bridge networks per compose project. - If that CLI command were to add
--network name-of-compose-networkfor it to join (or you create your own viadocker network create ...), this will provide parity as the legacy bridge is treated differently.
Notably, without the custom network assigned, the hostname command with --fqdn/--domain/--alias/--ip-address will fail if there's no entry in /etc/hosts for the sysctl kernel.hostname configured (--hostname). You can verify this by trying to perform a DNS A record query to the hostname which will be missing. Meanwhile with custom networks that you get out of the box with Docker Compose, you would get the IP of the container for that containers hostname via Docker's embedded DNS service, hence the mentioned hostname options that would otherwise have failed would still succeed with a custom network used.
This is a difference worth noting as if there is no valid DNS to resolve, our usage of hostname --fqdn would fail - yet it would be successful when there's a valid match in /etc/hosts. The short hostname is not relevant for that query, only the sysctl kernel.hostname seems to be used.
Example script
Either of these in user-patches.sh would work:
#!/bin/bash
# Prepends the desired hostname before the known hostname of the matched container in `/etc/hosts`:
# NOTE: It is required to preserve the inode of `/etc/hosts` due to the container using an implicit bind mount for this file into the container.
# - `sed` with `-i` / `--in-place` or `mv` commands are examples that replace the original files inode.
# - Use `nano`, `cp`, or `sponge` to replace file content instead as these will update the original file (preserving the inode).
function replace_fqdn_sed() {
local CURRENT_HOSTNAME="$(hostname --fqdn)"
local TARGET_HOSTNAME="${OVERRIDE_HOSTNAME}"
if [[ "${CURRENT_HOSTNAME}" != "${TARGET_HOSTNAME}" ]]; then
sed -E "s|(${CURRENT_HOSTNAME})|${TARGET_HOSTNAME} \1|" /etc/hosts > /tmp/hosts.tmp
cp /tmp/hosts.tmp /etc/hosts
rm /tmp/hosts.tmp
fi
}
# Requires the `moreutils` package for the `sponge` command:
function replace_fqdn_sponge() {
local CURRENT_HOSTNAME="$(hostname --fqdn)"
local TARGET_HOSTNAME="${OVERRIDE_HOSTNAME}"
if [[ "${CURRENT_HOSTNAME}" != "${TARGET_HOSTNAME}" ]]; then
sed -E "s|(${CURRENT_HOSTNAME})|${TARGET_HOSTNAME} \1|" /etc/hosts | sponge /etc/hosts
fi
}
# Call one of the functions above to update /etc/hosts:
replace_fqdn_sed
replace_fqdn_sponge
NOTE:
- That won't update
HOSTNAMEENV in the shell session (neither doeshostname example.testwhen usingCAP_SYS_ADMIN), so keep that in mind. - When our
user-patches.shscript is run, our internal scripts have already altered theHOSTNAMEENV (until we refactor in future to adoptDMS_FQDNinstead).
Considering the above information, this is probably important for us to manage as part of the OVERRIDE_HOSTNAME support by patching /etc/hosts when possible? 🤔
As I don't use Kubernetes, I would appreciate it if either @Xnyle @georglauterbach or @cfis could confirm /etc/hosts containing an appropriate entry with the hostname given to the container exists. Presumably there will be an IP assigned to the container with the hostname it was assigned (random hexadecimal value)?
If that is the case in k8s like it is with Docker Compose (when no hostname is explicitly configured), we could match on that as demonstrated below.
Docker + --network=host
FWIW: Docker does not add any hostname when using --network host as there is no docker managed network for that container to have it's own private IP address assigned with a DNS name to resolve to it.
Failing to find a match, we could potentially prepend to 127.0.0.1 localhost 🤔 which may be ok? Depends on what software is interacting with that. Technically with DNS as a fallback, updating /etc/hosts shouldn't be necessary, if sysctl kernel.hostname is configured to the public/private DNS name? Only when the hostname cannot be configured.
For Docker I was able to run a container with host mode networking, while configuring the hostname. The main difference is that the custom hostname is not added to /etc/hosts.
On WSL2 with a Windows 11 host running Docker Desktop, the container is run within the managed Docker WSL2 instance, so the default hostname with --network host is docker-desktop which would not be valid FQDN. Thus the hostname --fqdn (will fail if unable to resolve) / hostname (will not fail) commands could be used to add that when there is no entry in /etc/hosts, and OVERRIDE_HOSTNAME could prepend to that so that hostname --fqdn outputs that result.
$ hostname
docker-desktop
$ hostname --fqdn
docker-desktop
# Empty:
$ hostname --domain
$ hostname --short
docker-desktop
# Empty:
$ hostname --alias
# DNS resolution delay, followed by empty output:
# (might be querying all IP from `hostname --all-ip-addresses`)
$ hostname --all-fqdn
# Default network interface route of the container host I think? (WSL2 instance in this case)
# This IP is from the Docker Desktop configured subnet
$ hostname --ip-address
192.168.65.7
As there is no /etc/hosts match for docker-desktop, the results would be dependent upon DNS from nsswitch.conf being successful.
Modify /etc/hosts to include docker-desktop for resolving, but hello.example.test as the first hostname as the FQDN we want to resolve:
127.0.0.1 hello.example.test localhost docker-desktop
::1 localhost ip6-localhost ip6-loopback
fe00::0 ip6-localnet
ff00::0 ip6-mcastprefix
ff02::1 ip6-allnodes
ff02::2 ip6-allrouters
$ docker run --rm -it --network host docker:12-slim bash
$ hostname
docker-desktop
$ hostname --fqdn
hello.example.test
$ hostname --domain
example.test
$ hostname --short
docker-desktop
$ hostname --alias
localhost docker-desktop
# DNS resolution delay, followed by empty output:
$ hostname --all-fqdn
$ hostname --ip-address
127.0.0.1
kubernetes ingress needing to preserve the client IP from external traffic via PROXY protocol
There is no Ingress? Just a k8s Service and with the Service being of type LoadBalancer while using MetalLB you get the correct IP delivered to your (container) service out of the box. So I didn't bother with PROXY proto.
Also I don't know what kind of voodoo Docker compose does in terms of ipv4/ipv6 translation but I don't think it applies to k8s? Or at least not to MetalLB?
Regarding unauthorized access from outside, even "host" setting wouldn't help much, You probably need custom config for you very network anyway (https://docker-mailserver.github.io/docker-mailserver/latest/config/advanced/override-defaults/postfix/)
Roundcube problem solved btw. as you can just configure it to use the IMAP Login for SMTP as well.
SYS_ADMIN
Is a bad idea IMHO, just pursue other solutions ;-)
kubernetes ingress needing to preserve the client IP from external traffic via PROXY protocolThere is no Ingress? Just a k8s Service and with the Service being of type LoadBalancer while using MetalLB you get the correct IP delivered to your (container) service out of the box. So I didn't bother with PROXY proto.
🤷♂ Like I said kubernetes isn't my area of expertise. I have had it requested though because of kubernetes where public traffic was coming in through Traefik to reach a container in a pod or something like that where the client IP needed to be preserved when it reached DMS, but using PROXY protocol with DMS then needs additional ports for other internal containers that connect to the DMS container directly.
I can't comment much on that further as I am not familiar enough with k8s networking.
Also I don't know what kind of voodoo Docker compose does in terms of ipv4/ipv6 translation but I don't think it applies to k8s? Or at least not to MetalLB?
Docker has NAT for IPv4 and IPv6 (optional) for traffic coming in from the host public network into the private subnet that docker manages for it's containers.
The default networking has userland-proxy enabled which is a service that alters iptables routing for some conveniences, such as IPv6 public client connections being routed to an IPv4 only network internally for the DMS container. With userland-proxy disabled that type of connection would fail by default, unless the DMS container was also added to an IPv6 network. No voodoo.
If you have a load balancer or other service that proxies traffic (be that a reverse proxy you host or cloudflare spectrum IIRC), that is another service inbetween the real client device and DMS, the original client IP will not be preserved unless you use PROXY protocol.
Again I'm not familiar with kubernetes networking, so if that's not the case for you, perhaps it's doing it's own "voodoo" in that kind of setup 🤔
Generally when possible I advise avoiding the indirection for DMS since nothing else should really need to share the mail ports they can be exposed to public traffic directly.
Roundcube problem solved btw. as you can just configure it to use the IMAP Login for SMTP as well.
There is login to Roundcube itself and login from roundcube to Dovecot (Postfix delegates to Dovecot via SASL). Connection from Roundcube to Dovecot is a single IP, unless you have something to preserve the original client IP, then it all appears from the same source.
Roundcube has some plugin with IMAP to workaround that but it requires some modification to Dovecot support IIRC.
Alternatively PROXY protocol can be used, you must configure Dovecot to trust Roundcube as you'd be giving it the ability to manipulate the PROXY protocol header to whatever IP source it likes to claim the traffic as coming from.
SYS_ADMINIs a bad idea IMHO, just pursue other solutions ;-)
I never said it was good. I'm just documenting why the hostname couldn't be configured within the container by default via the hostname command despite technically being root.
The other solutions aren't perfect either, depends on how software chooses to acquire the hostname of the container if it needs it.
Traefik to reach a container in a pod or something
Yeah thats an Ingress in k8s terms, a full blown Layer 6/7 application that translates and routes HTTP/HTTPS traffic, in conjunction with a cert-manager automatically handles TLS/SSL termination, etc.
Not used at all for dms-helm.
Services are Layer 4/5 and a LoadBalancer is Layer 2/3 in order to get your traffic in/out of those to be exposed services. There is a bit of nftables voodoo but no address family translation or even proxying.
So you don't loose the source address and don't need proxy workarounds. (Unless you have something in front of that that does SNAT but thats then not the fault of your k8s stack).
As I don't use Kubernetes, I would appreciate it if either @Xnyle
cat /etc/hosts
# Kubernetes-managed hosts file.
127.0.0.1 localhost
::1 localhost ip6-localhost ip6-loopback
fe00::0 ip6-localnet
fe00::0 ip6-mcastprefix
fe00::1 ip6-allnodes
fe00::2 ip6-allrouters
10.76.0.15 docker-mailserver-57b88777b-l7744
Also: (so those are not bind mounted host files but generated)
/dev/disk/by-uuid/xyz.. on /etc/hosts type ext4 (rw,relatime)
/dev/disk/by-uuid/xyz.. on /etc/hostname type ext4 (rw,relatime)
/dev/disk/by-uuid/xyz.. on /etc/resolv.conf type ext4 (rw,relatime)
10.76 0.0/24 is the pod network. As there is only one pod and only one container (design decison), there can't be other devices on that subnet.
PERMIT_DOCKER=connected-networks would then be basically the same as just my_ip. PERMIT_DOCKER=host would make a /16 out of that which in k8s would add a mask for all containers in all pods of the whole cluster but not the host network itself. PERMIT_DOCKER=network is a hack and the warning in the docs should not be about that docker might use other addresses but rather that you open your relay with that to potentially unwanted networks in half of the 172 range.
Traefik to reach a container in a pod or something
Yeah thats an Ingress in k8s terms, a full blown Layer 6/7 application that translates and routes HTTP/HTTPS traffic, in conjunction with a cert-manager automatically handles TLS/SSL termination, etc.
Not used at all for dms-helm.
Traefik handles Layer 4 with just TCP when routing to DMS ports no?
I believe I've seen users relying on Traefik for managing the certs on the Docker side, not too sure about with k8s. Regardless DMS expects to terminate TLS, so any such connections are effectively passthrough in Traefik, but PROXY protocol would still be leveraged.
So you don't loose the source address and don't need proxy workarounds. (Unless you have something in front of that that does SNAT but thats then not the fault of your k8s stack).
Apologies, perhaps I made a mistake with the jargon by referring to ingress then 😓 (this comment also seems to clarify to a user that no ingress is involved)
I recently provided feedback to a PR here that was revising the PROXY protocol support: https://github.com/docker-mailserver/docker-mailserver-helm/pull/156#issuecomment-2795344601
We also have this PROXY protocol feature request from Feb 2024 that came from the maintainer of the helm repo here. Quick comment reference:
- Thunderbird client used as an external client example, and some talk about k8s/pods/clusters
- Roundcube is mentioned, and 3 types of client connections to support for DMS
- Roundcube mentioned in regards to HTTPS traffic via ingress + internal traffic needing to avoid PROXY protocol usage + other k8s specific networking details
- A difference concern raised between self-hosted and managed k8s deployments
- Another maintainer (that uses k8s) chimes in, they use MetalLB and Cilium
So I don't quite grok all of that, but that supposedly clarifies why some deployments with k8s need PROXY protocol support? 🤷♂
Oh and our DMS k8s docs page also has a section for PROXY protocol:
So if there's a mistake there, such as referring to ingress with Traefik/Nginx in this context let me know :)
10.76.0.15 docker-mailserver-57b88777b-l7744
Awesome, thanks!
Is that the same hostname value hostname --fqdn would return when run in the container? (presumably cat /proc/sys/kernel/hostname too?)
Also: (so those are not bind mounted host files but generated)
Thanks, on the docker container side I'm pretty sure they're bind mounts, they at least act like a file being bind mounted 😅 (could be something else causing the error with sed / mv)
So if we add a solution it would need to preserve the inode so that the change can be applied in that environment.
Regarding the feedback on PERMIT_DOCKER, as per the issue I linked you to, there's really not much to discuss there.
Someone could take the time to contribute to our docs for better clarity, or try tackle a refactor of the feature to address the known flaws the linked issue already documents. Otherwise I'll eventually get to it myself.
In the meantime I discourage using PERMIT_DOCKER since you shouldn't really ever need it, just giving clients credentials to authenticate with works well (with fetchmail it's not because it's attempting standard delivery to port 25 I think, rather than authenticated submission which should skip the HELO check just like PERMIT_DOCKER would). The feature itself is legacy and only really exists as an escape hatch for troubleshooting.
Traefik handles Layer 4 with just TCP when routing to DMS ports no? I believe I've seen users relying on Traefik for managing the certs on the Docker side
No idea what users do with docker, it's a dead horse to me ;-)
Traefik or any other Ingess is in practice not involved in anything else than HTTP(S) communication. I don't know if the k8s concept allows other uses, but I've never seen that, probably because there simply is no mainstream software that does for instance "smtp/imap reverse proxying", what would that even be?
The routing is done via a CNI-Plugin like flannel and the "ingress" into the cluster network is (if not done via NodePort) via a LoadBalancer.
Certs (in dms-helm) are managed either manually or via a cert-manager but even that doesn't use an Ingress as no external resources need to talk to it (via HTTP).
probably because there simply is no mainstream software that does for instance "smtp/imap reverse proxying", what would that even be?
Uhh you can do that with Caddy, Nginx or Traefik? Traffic comes into them on the public ports and they route to the actual service private ports.
I think Nginx has improved support for StartTLS, but for standard implicit TLS you can route by SNI:
Depending on what you're setup is, you could potentially workaround the starttls ports concern for routing too, similar to how you can for reverse proxying SSH by SNI (outbound connection from the client is TLS wrapped via proxy command setting, reverse proxy inspects SNI and terminates the connection to unwrap it before forwarding to the desired service).
But the more common case is what I mentioned, if you have other hops between DMS and the connecting client (IP you want to preserve), you need to have the PROXY protocol used from the service that the real client connects to, then any forwarded TCP traffic has that information when it arrives to DMS (configured to trust the proxy service(s)).