Skip to content

Conversation

@springcomp
Copy link
Owner

@springcomp springcomp commented Jan 1, 2026

This PR adds support for Postfix to check STMP DNS-base Authentication of Named Entites (DANE) when sending outgoing emails.

postfix/templates/30-icf-dane.tpl

smtp_dns_support_level = dnssec
smtp_tls_security_level = dane

For reliability, support for DANE in Postfix MUST take advantage of a DNSSEC validating name server under our control. This PR therefore also includes the mvance/unbound:1.22.0 running inside the docker-compose stack.

For some reason, docker-compose does not support specifying the DNS server of a service by name.

  services:

    dns:
      image: mvance/unbound:1.22.0
      container_name: dns
+     networks:
+       internal:
+         ipv4_address: '10.0.0.99'
      volumes:
        - ./unbound/conf.d/:/opt/unbound/etc/unbound/:rw
      restart: unless-stopped

    postfix:
      image: private/postfix:latest
      …
      dns:
-       - 'dns'  
+       - '10.0.0.99'  

Unfortunately, there seems to be no way to set a static ip for a service using the default bridge network.
Therefore, this PR changes the default network to a custom network named internal instead.

  networks:
-   default:
+   internal:
      driver: bridge
      ipam:
        driver: default
        config:
          - subnet: 10.0.0.0/24
            gateway: 10.0.0.1

This PR also assigns a specific network to all services where the default network was implicitely selected:

    …

    migration:
      <<: *sl-defaults
      command: [ "alembic", "upgrade", "head" ]
      container_name: sl-migration
+     networks:
+      - internal
      depends_on:
        postgres:
          condition: service_healthy

    init:
      <<: *sl-defaults
      command: [ "python", "init_app.py" ]
      container_name: sl-init
+     networks:
+      - internal
  …

    app:
      <<: *sl-defaults
      container_name: sl-app
      networks:
        - traefik
-       - default
+       - internal
    …
    email:
      <<: *sl-defaults
      command: ["python", "email_handler.py"]
      container_name: sl-email
+     networks:
+      - internal
  …

    job-runner:
      <<: *sl-defaults
      command: ["python", "job_runner.py"]
      container_name: sl-job-runner
+     networks:
+      - internal

@chrisblech
Copy link
Contributor

Happy new year 🎉 - this PR looks great at my first sight 🤩

Here some thoughts (not yet tested) to keep it a little bit more simple:

  • move the "networks: internal" lines up to the "sl-defaults"
  • geek idea: use 10.0.0.53 for DNS ?
  • use mvance/unbound instead of building another private image

mvance/unbound is focused on security, updated synchronous to unbound, and is often recommended in combination with postfix. It is configured for DNSSEC by default, and auto-renews root.hints and trust-anchor. Also logging is optimized for container environments.

@springcomp
Copy link
Owner Author

springcomp commented Jan 1, 2026

Hey @chrisblech my best wishes to you too !

* move the "networks: internal" lines up to the "sl-defaults"> 
* geek idea: use 10.0.0.53 for DNS ?

Those are great ideas !

* use [mvance/unbound](https://hub.docker.com/r/mvance/unbound) instead of building another private image

I’m trying to have unbound log to stdout but fail to do so. However, it only supports logging to a file.
That’s why I create the log file as a symlink to stdout. I think this is necessary – at least, initially – to troubleshoot and make sure that the DNS is indeed wired up correctly.

If I configure unbound to log to /dev/stdout it crashes on startup. The symlink trick has unbound print an error on startup but otherwise it starts correctly and work successfully.

Based on your comments, I’ll revisit and see if I made some mistakes on my first try.

@chrisblech
Copy link
Contributor

After digging into this topic a little deeper, I realized that "mvance/unbound" seems to be abandoned for two years now. Some research later, I got this setup working (without need to build a custom image, and logging to stdout):

services:
  unbound:
    image: crazymax/unbound:${UNBOUND_VERSION:-latest}
    restart: unless-stopped
    user: "0:0"
    cap_add:
      - NET_BIND_SERVICE
    networks:
      internal:
        ipv4_address: 10.0.0.53    
    entrypoint:
      - /bin/sh
      - -ec
      - |
        cat > /config/00-listen-port.conf <<'EOF'
        server:
          interface: 0.0.0.0@53
          log-queries: yes
          verbosity: 2
        EOF
        unbound-anchor -a /var/run/unbound/root.key || true
        exec su -s /bin/sh unbound -c "sh /entrypoint.sh"

@springcomp
Copy link
Owner Author

springcomp commented Jan 4, 2026

@chrisblech thanks for investigating.

This PR now replaces the private mvance/unbound-based image with the default crazymax/unbound image as per your suggestion.

For some reason, the root.hints file was not picked up automatically in line with it not being specified in the provided unbound.conf default file.

So I just referred to the one hosted in the Alpine image :

+  # root.hints enable unbound to perform recursive resolution
+  root-hints: "/usr/share/dns-root-hints/named.root"

Also, I had a warning upon startup that could not honor the requested socket buffer and instructed me to set so-sndbuf to 0 🤷‍♂️

    # Larger socket buffer. OS may need config.
    # Ensure kernel buffer is large enough to not lose messages in traffic spikes
-   #so-sndbuf: 4m
+   so-sndbuf: 0

Overall, this works quite nicely.

@chrisblech
Copy link
Contributor

@springcomp great 🤩 this looks much more streamlined than before.

I also saw the warning regarding socket buffer size, but decided to leave it unchanged. So it will warn on startup, and the decision stays in the responsibility of upstream developer (unbound - to set good defaults) and machine operator who knows their needs and resources, and can change this setting in unbound, or system-wide.

As "crazymax" already loads the complete config file (with all its documentation, and quite useful defaults imho), I would suggest to drop the whole 00-unbound.conf file in this PR, and just provide small snippets for our necessary changes (listening port) - this would make it again more clean and simple.

@chrisblech
Copy link
Contributor

Hint: after dropping 00-unbound.conf, I needed to set port 53 via override interface: 0.0.0.0@53 as port: 53 did not work

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants