diff --git a/Dockerfile b/Dockerfile index 2a98e48..067c6d1 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,19 +1,99 @@ -FROM ubuntu:22.04 +# Build and runtime stages for OpenDKIM on Ubuntu 16.04 +FROM ubuntu:16.04 AS builder -WORKDIR /opt/opendkim +ARG DEBIAN_FRONTEND=noninteractive +ARG OPENDKIM_VERSION=2.10.3 +ARG OPENDKIM_TARBALL=opendkim-${OPENDKIM_VERSION}.tar.gz +ARG OPENDKIM_URL=https://downloads.sourceforge.net/project/opendkim/${OPENDKIM_TARBALL} -RUN apt update && \ - apt upgrade -y && \ - apt install -y opendkim inetutils-syslogd curl +RUN apt-get update && apt-get install -y --no-install-recommends \ + build-essential \ + ca-certificates \ + wget \ + tar \ + gzip \ + make \ + autoconf \ + automake \ + libtool \ + pkg-config \ + libssl-dev \ + libmilter-dev \ + libbsd-dev \ + zlib1g-dev \ + && rm -rf /var/lib/apt/lists/* -RUN curl -SsfL -o /usr/bin/gomplate "https://github.com/hairyhenderson/gomplate/releases/download/v3.11.5/gomplate_linux-amd64-slim" && \ - chmod 755 /usr/bin/gomplate && \ - mkdir -p /etc/rsyslog.d/ && \ - touch /etc/rsyslog.d/stdout.conf && \ - echo "*.* /dev/stdout" > /etc/rsyslog.d/stdout.conf +WORKDIR /build -COPY entrypoint.sh . -COPY opendkim.conf.tpl . +RUN wget -O ${OPENDKIM_TARBALL} ${OPENDKIM_URL} \ + && tar xzf ${OPENDKIM_TARBALL} -EXPOSE 8892/tcp -CMD ["/bin/bash", "entrypoint.sh"] \ No newline at end of file +WORKDIR /build/opendkim-${OPENDKIM_VERSION} + +# Basic build: +# - prefix /usr +# - config in /etc/opendkim +# - pid file under /run/opendkim +RUN ./configure \ + --prefix=/usr \ + --sysconfdir=/etc \ + --localstatedir=/var \ + --disable-shared \ + --enable-static + +RUN make -j"$(nproc)" + +# Install into staging root +RUN make DESTDIR=/opt/opendkim-dist install + +# Collect runtime libs actually needed by installed binaries +RUN mkdir -p /opt/opendkim-libs \ + && find /opt/opendkim-dist -type f -executable -exec ldd {} \; \ + | awk '/=> \// {print $3} /^\// {print $1}' \ + | sort -u \ + | xargs -r -I '{}' cp -v --parents '{}' /opt/opendkim-libs + +# Pack staged install and copied libs as tarballs to avoid BuildKit COPY-to-/ issues +RUN cd /opt/opendkim-dist \ + && tar -cf /opt/opendkim-dist.tar . + +RUN cd /opt/opendkim-libs \ + && tar -cf /opt/opendkim-libs.tar . + +# Final runtime image +FROM ubuntu:16.04 + +ARG DEBIAN_FRONTEND=noninteractive + +RUN apt-get update && apt-get install -y --no-install-recommends \ + ca-certificates \ + ssl-cert \ + inetutils-syslogd \ + && rm -rf /var/lib/apt/lists/* + +COPY --from=builder /opt/opendkim-dist.tar /tmp/opendkim-dist.tar +COPY --from=builder /opt/opendkim-libs.tar /tmp/opendkim-libs.tar + +RUN tar -C / -xf /tmp/opendkim-dist.tar \ + && tar -C / -xf /tmp/opendkim-libs.tar \ + && rm -f /tmp/opendkim-dist.tar /tmp/opendkim-libs.tar + +RUN groupadd -f opendkim \ + && (id -u opendkim >/dev/null 2>&1 || useradd -r -g opendkim -d /var/lib/opendkim -s /usr/sbin/nologin opendkim) + +RUN mkdir -p \ + /etc/opendkim \ + /etc/opendkim/keys \ + /run/opendkim \ + /var/lib/opendkim \ + && chown -R opendkim:opendkim /etc/opendkim/keys /run/opendkim /var/lib/opendkim + +RUN touch /etc/opendkim/KeyTable /etc/opendkim/SigningTable /etc/opendkim/TrustedHosts \ + && chown opendkim:opendkim /etc/opendkim/KeyTable /etc/opendkim/SigningTable /etc/opendkim/TrustedHosts + +COPY entrypoint.sh /usr/local/bin/entrypoint.sh +RUN chmod +x /usr/local/bin/entrypoint.sh + +EXPOSE 8891 + +ENTRYPOINT ["/usr/local/bin/entrypoint.sh"] diff --git a/README.md b/README.md index 20cf604..057d427 100644 --- a/README.md +++ b/README.md @@ -4,55 +4,215 @@ docker build --rm -t opendkim:latest . ``` # Generating private key -Before running the private key must be generated using opendkim-keygen or supplied. -```sh -# Generate private key. -opendkim-genkey --bits=2048 --selector=dkim --restrict --verbose +Before running the container, a private key must be generated using `opendkim-genkey` or supplied manually. -# Getting publickey for DNS record. +```sh +# Generate private key and DNS record files. +opendkim-genkey --bits=2048 --selector=dkim --restrict --verbose --domain=example.com + +# Generated files: +# dkim.private +# dkim.txt +``` + +To extract the public key value for a DNS TXT record: + +```sh cat dkim.txt | tr -d "\"\n\" \t" | sed -r "s/.*\((.*)\).*/\\1\n/" ``` +Example DNS record name: + +```txt +dkim._domainkey.example.com +``` + # Running the image ```sh -docker run -it --rm --name opendkim -p 8892:8892 -v /path/dkim.private:/opt/opendkim/keys/dkim.private opendkim:latest +docker run -it --rm \ + --name opendkim \ + -p 8892:8892 \ + -v /path/to/dkim.private:/opt/opendkim/keys/dkim.private:ro \ + opendkim:latest +``` + +Example with custom domain and selector: + +```sh +docker run -it --rm \ + --name opendkim \ + -p 8892:8892 \ + -e OPENDKIM_DOMAIN=example.com \ + -e OPENDKIM_SELECTOR=mail \ + -e OPENDKIM_SOCKET="inet:8892@0.0.0.0" \ + -v /path/to/mail.private:/opt/opendkim/keys/dkim.private:ro \ + opendkim:latest ``` # Environment variables -These values are default and can be overriden by declaring environment variable with naother value. -```sh -# Attempts to become the specified userid before starting operations. The value is of the form userid[:group]. -OPENDKIM_USERID="opendkim" +These values are defaults and can be overridden by setting environment variables. -# Specifies the socket that should be established by the filter to receive connections. +```sh +# Runtime user name. +OPENDKIM_USER="opendkim" + +# Runtime group name. +OPENDKIM_GROUP="opendkim" + +# User and group used by OpenDKIM. Format: user:group +OPENDKIM_USERID="opendkim:opendkim" + +# Socket used by the milter service. OPENDKIM_SOCKET="inet:8892@0.0.0.0" -# A set of domains whose mail should be signed by this filter. +# Domain whose mail should be signed. OPENDKIM_DOMAIN="*" -# Gives the location of a PEM-formatted private key to be used for signing all messages. Ignored if a KeyTable is defined. -OPENDKIM_KEYFILE="/opt/opendkim/keys/dkim.private" - -# Defines the name of the selector to be used when signing messages. +# DKIM selector. OPENDKIM_SELECTOR="dkim" -# Selects the canonicalization method(s) to be used when signing messages. +# Path to private key mounted into the container. +OPENDKIM_KEYFILE="/opt/opendkim/keys/dkim.private" + +# Canonicalization method used when signing. OPENDKIM_CANONICALIZATION="relaxed/simple" -# Selects operating modes. The string is a concatenation of characters -# that indicate which mode(s) of operation are desired. Valid modes are s (signer) and v (verifier). +# Operating mode: +# s = signer +# v = verifier +# sv = sign and verify OPENDKIM_MODE="sv" -# Sign subdomains of those listed by the Domain parameter as well as the actual domains. +# Whether to sign subdomains too. OPENDKIM_SUBDOMAINS="true" -# Specifies a set of header fields that should be included in all signature header lists (the "h=" tag) -# once more than the number of times they were actually present in the signed message. +# Headers to oversign. OPENDKIM_OVERSIGNHEADERS="From" -# Specifies a file from which trust anchor data should be read when doing DNS queries and applying the DNSSEC protocol. -OPENDKIM_TRUSTANCHORFILE="/usr/share/dns/root.key" +# Optional DNSSEC trust anchor file. +# Added to config only if the file exists. +OPENDKIM_TRUSTANCHORFILE="" -# Identifies a set internal hosts whose mail should be signed rather than verified. -OPENDKIM_INTERNALHOSTS="127.0.0.0/8, 192.168.0.0/16, 172.16.0.0/12, 10.0.0.0/8" -``` \ No newline at end of file +# Internal hosts whose mail should be signed instead of verified. +OPENDKIM_INTERNALHOSTS="127.0.0.1,localhost,127.0.0.0/8,192.168.0.0/16,172.16.0.0/12,10.0.0.0/8" + +# ExternalIgnoreList value for OpenDKIM. +OPENDKIM_EXTERNALIGNORELIST="refile:/etc/opendkim/TrustedHosts" + +# Path to file used for InternalHosts. +OPENDKIM_INTERNALHOSTS_FILE="/etc/opendkim/TrustedHosts" + +# Path to KeyTable. +OPENDKIM_KEYTABLE="/etc/opendkim/KeyTable" + +# Path to SigningTable. +OPENDKIM_SIGNINGTABLE="refile:/etc/opendkim/SigningTable" + +# PID file path. +OPENDKIM_PIDFILE="/run/opendkim/opendkim.pid" + +# Umask used by OpenDKIM. +OPENDKIM_UMASK="002" + +# Whether OpenDKIM should auto-restart. +OPENDKIM_AUTO_RESTART="no" + +# Auto restart rate. +OPENDKIM_AUTO_RESTART_RATE="10/1h" + +# DNS query timeout in seconds. +OPENDKIM_DNS_TIMEOUT="5" + +# Signing algorithm. +OPENDKIM_SIGNATURE_ALGORITHM="rsa-sha256" + +# Refuse unsafe private key permissions. +OPENDKIM_REQUIRE_SAFE_KEYS="yes" + +# Remove old signatures before signing. +OPENDKIM_REMOVE_OLD_SIGNATURES="no" + +# Add SoftwareHeader. +OPENDKIM_LOGRESULTS="yes" + +# Milter debug level. +OPENDKIM_MILTER_DEBUG="6" + +# Optional custom nameservers. +OPENDKIM_NAMESERVERS="" +``` + +# Behavior +At startup the container: + +- creates OpenDKIM runtime directories +- copies the mounted private key to `/var/opendkim/dkim.private` +- sets secure ownership and permissions on the copied key +- generates `TrustedHosts`, `KeyTable`, and `SigningTable` if they are empty +- generates `/etc/opendkim.conf` from environment variables +- starts OpenDKIM using `/etc/opendkim.conf` + +# Generated files +The entrypoint generates these files automatically: + +```txt +/etc/opendkim.conf +/etc/opendkim/TrustedHosts +/etc/opendkim/KeyTable +/etc/opendkim/SigningTable +/var/opendkim/dkim.private +``` + +# Default generated tables +For example, with: + +```sh +OPENDKIM_DOMAIN=example.com +OPENDKIM_SELECTOR=dkim +``` + +the generated files look like this: + +## /etc/opendkim/KeyTable +```txt +dkim._domainkey.example.com example.com:dkim:/var/opendkim/dkim.private +``` + +## /etc/opendkim/SigningTable +```txt +*@example.com dkim._domainkey.example.com +``` + +## /etc/opendkim/TrustedHosts +```txt +127.0.0.1 +localhost +127.0.0.0/8 +192.168.0.0/16 +172.16.0.0/12 +10.0.0.0/8 +``` + +# Postfix example +Example Postfix settings when OpenDKIM runs in another container named `opendkim`: + +```conf +smtpd_milters = inet:opendkim:8892 +non_smtpd_milters = inet:opendkim:8892 +milter_protocol = 6 +milter_default_action = accept +``` + +# Notes +- The private key must be mounted into the container at the path specified by `OPENDKIM_KEYFILE`. +- The entrypoint copies the private key to `/var/opendkim/dkim.private` and locks down permissions to `0600`. +- `OPENDKIM_TRUSTANCHORFILE` is optional and is only written to config if the file exists. +- `OPENDKIM_NAMESERVERS` is optional and is only written to config if non-empty. +- `OPENDKIM_DOMAIN="*"` is allowed by the script, but for real signing setups you usually want a concrete domain such as `example.com`. +- The default socket is `inet:8892@0.0.0.0`, so map port `8892` unless you override `OPENDKIM_SOCKET`. +- The current entrypoint starts OpenDKIM with: + ```sh + ./usr/sbin/opendkim -x /etc/opendkim.conf + syslogd -n -f /etc/rsyslog.d/stdout.conf + ``` + so the image expects syslog configuration to be present. \ No newline at end of file diff --git a/entrypoint.sh b/entrypoint.sh index aa97a36..951728c 100644 --- a/entrypoint.sh +++ b/entrypoint.sh @@ -1,25 +1,125 @@ #!/bin/bash -# Misc default variables. -export OPENDKIM_USERID=${OPENDKIM_USERID:-opendkim} -export OPENDKIM_SOCKET=${OPENDKIM_SOCKET:-inet:8892@0.0.0.0} -export OPENDKIM_DOMAIN=${OPENDKIM_DOMAIN:-*} -export OPENDKIM_KEYFILE=${OPENDKIM_KEYFILE:-/opt/opendkim/keys/dkim.private} -export OPENDKIM_SELECTOR=${OPENDKIM_SELECTOR:-dkim} -export OPENDKIM_CANONICALIZATION=${OPENDKIM_CANONICALIZATION:-relaxed/simple} -export OPENDKIM_MODE=${OPENDKIM_MODE:-sv} -export OPENDKIM_SUBDOMAINS=${OPENDKIM_SUBDOMAINS:-true} -export OPENDKIM_OVERSIGNHEADERS=${OPENDKIM_OVERSIGNHEADERS:-From} -export OPENDKIM_TRUSTANCHORFILE=${OPENDKIM_TRUSTANCHORFILE:-/usr/share/dns/root.key} -export OPENDKIM_INTERNALHOSTS=${OPENDKIM_INTERNALHOSTS:-127.0.0.0/8, 192.168.0.0/16, 172.16.0.0/12, 10.0.0.0/8} +# Core defaults. +: "${OPENDKIM_USER:=opendkim}" +: "${OPENDKIM_GROUP:=opendkim}" +: "${OPENDKIM_USERID:=${OPENDKIM_USER}:${OPENDKIM_GROUP}}" +: "${OPENDKIM_SOCKET:=inet:8892@0.0.0.0}" -# Configuration templates. -gomplate -f opendkim.conf.tpl > /opt/opendkim/opendkim.conf +# Signing defaults. +: "${OPENDKIM_DOMAIN:=*}" +: "${OPENDKIM_SELECTOR:=dkim}" +: "${OPENDKIM_KEYFILE:=/opt/opendkim/keys/dkim.private}" +: "${OPENDKIM_CANONICALIZATION:=relaxed/simple}" +: "${OPENDKIM_MODE:=sv}" +: "${OPENDKIM_SUBDOMAINS:=true}" +: "${OPENDKIM_OVERSIGNHEADERS:=From}" -mkdir -p /var/opendkim -cp $OPENDKIM_KEYFILE /var/opendkim/dkim.private -chown opendkim:opendkim /var/opendkim/dkim.private -chmod 0600 /var/opendkim/dkim.private +# DNS / trust defaults. +: "${OPENDKIM_TRUSTANCHORFILE:=}" +: "${OPENDKIM_INTERNALHOSTS:=127.0.0.1,localhost,127.0.0.0/8,192.168.0.0/16,172.16.0.0/12,10.0.0.0/8}" +: "${OPENDKIM_EXTERNALIGNORELIST:=refile:/etc/opendkim/TrustedHosts}" +: "${OPENDKIM_INTERNALHOSTS_FILE:=/etc/opendkim/TrustedHosts}" -opendkim -x /opt/opendkim/opendkim.conf -syslogd -n -f /etc/rsyslog.d/stdout.conf \ No newline at end of file +# Table files. +: "${OPENDKIM_KEYTABLE:=/etc/opendkim/KeyTable}" +: "${OPENDKIM_SIGNINGTABLE:=refile:/etc/opendkim/SigningTable}" +: "${OPENDKIM_PIDFILE:=/run/opendkim/opendkim.pid}" + +# Behavior. +: "${OPENDKIM_UMASK:=002}" +: "${OPENDKIM_BACKGROUND:=no}" +: "${OPENDKIM_AUTO_RESTART:=no}" +: "${OPENDKIM_AUTO_RESTART_RATE:=10/1h}" +: "${OPENDKIM_DNS_TIMEOUT:=5}" +: "${OPENDKIM_SIGNATURE_ALGORITHM:=rsa-sha256}" + +# Optional extras. +: "${OPENDKIM_REQUIRE_SAFE_KEYS:=yes}" +: "${OPENDKIM_REMOVE_OLD_SIGNATURES:=no}" +: "${OPENDKIM_LOGRESULTS:=yes}" +: "${OPENDKIM_MILTER_DEBUG:=6}" +: "${OPENDKIM_NAMESERVERS:=}" + +mkdir -p \ + /etc/opendkim \ + /run/opendkim \ + /var/lib/opendkim \ + /var/opendkim + +touch /etc/opendkim/TrustedHosts /etc/opendkim/KeyTable /etc/opendkim/SigningTable + +chown -R "${OPENDKIM_USER}:${OPENDKIM_GROUP}" /run/opendkim /var/lib/opendkim /var/opendkim +chmod 0755 /run/opendkim /var/lib/opendkim /var/opendkim + +# Copy private key to runtime location with safe permissions. +if [ -f "${OPENDKIM_KEYFILE}" ]; then + cp "${OPENDKIM_KEYFILE}" /var/opendkim/dkim.private + chown "${OPENDKIM_USER}:${OPENDKIM_GROUP}" /var/opendkim/dkim.private + chmod 0600 /var/opendkim/dkim.private +fi + +# Generate TrustedHosts from env if file is empty. +if [ ! -s /etc/opendkim/TrustedHosts ]; then + printf '%s\n' "${OPENDKIM_INTERNALHOSTS}" | tr ',' '\n' > /etc/opendkim/TrustedHosts +fi + +# Generate KeyTable from env if file is empty. +if [ ! -s /etc/opendkim/KeyTable ]; then + printf '%s._domainkey.%s %s:%s:/var/opendkim/dkim.private\n' \ + "${OPENDKIM_SELECTOR}" \ + "${OPENDKIM_DOMAIN}" \ + "${OPENDKIM_DOMAIN}" \ + "${OPENDKIM_SELECTOR}" \ + > /etc/opendkim/KeyTable +fi + +# Generate SigningTable from env if file is empty. +if [ ! -s /etc/opendkim/SigningTable ]; then + printf '*@%s %s._domainkey.%s\n' \ + "${OPENDKIM_DOMAIN}" \ + "${OPENDKIM_SELECTOR}" \ + "${OPENDKIM_DOMAIN}" \ + > /etc/opendkim/SigningTable +fi + +chown "${OPENDKIM_USER}:${OPENDKIM_GROUP}" /etc/opendkim/TrustedHosts /etc/opendkim/KeyTable /etc/opendkim/SigningTable + +cat > /etc/opendkim.conf <> /etc/opendkim.conf +fi + +if [ "${OPENDKIM_LOGRESULTS}" = "yes" ]; then + echo "SoftwareHeader yes" >> /etc/opendkim.conf +fi + +if [ -n "${OPENDKIM_NAMESERVERS}" ]; then + echo "Nameservers ${OPENDKIM_NAMESERVERS}" >> /etc/opendkim.conf +fi + +./usr/sbin/opendkim -x /etc/opendkim.conf +syslogd -n -f /etc/rsyslog.d/stdout.conf diff --git a/opendkim.conf.tpl b/opendkim.conf.tpl deleted file mode 100644 index 550adb4..0000000 --- a/opendkim.conf.tpl +++ /dev/null @@ -1,47 +0,0 @@ -# Disable log to syslog because we want to log in stdout. -Syslog true - -# Log via calls to syslog(3) additional entries indicating successful signing or verification of messages. -SyslogSuccess true - -# If logging is enabled (see Syslog below), issues very detailed logging about the -# logic behind the filter’s decision to either sign a message or verify it. -LogWhy true - -# Specifies the path to a file that should be created at process start containing the process ID. -PidFile /var/run/opendkim/opendkim.pid - -# Attempts to become the specified userid before starting operations. The value is of the form userid[:group]. -UserID {{ .Env.OPENDKIM_USERID }} - -# Specifies the socket that should be established by the filter to receive connections. -Socket {{ .Env.OPENDKIM_SOCKET }} - -# A set of domains whose mail should be signed by this filter. -Domain {{ .Env.OPENDKIM_DOMAIN }} - -# Gives the location of a PEM-formatted private key to be used for signing all messages. Ignored if a KeyTable is defined. -KeyFile /var/opendkim/dkim.private - -# Defines the name of the selector to be used when signing messages. -Selector {{ .Env.OPENDKIM_SELECTOR }} - -# Selects the canonicalization method(s) to be used when signing messages. -Canonicalization {{ .Env.OPENDKIM_CANONICALIZATION }} - -# Selects operating modes. The string is a concatenation of characters -# that indicate which mode(s) of operation are desired. Valid modes are s (signer) and v (verifier). -Mode {{ .Env.OPENDKIM_MODE }} - -# Sign subdomains of those listed by the Domain parameter as well as the actual domains. -SubDomains {{ .Env.OPENDKIM_SUBDOMAINS }} - -# Specifies a set of header fields that should be included in all signature header lists (the "h=" tag) -# once more than the number of times they were actually present in the signed message. -OversignHeaders {{ .Env.OPENDKIM_OVERSIGNHEADERS }} - -# Specifies a file from which trust anchor data should be read when doing DNS queries and applying the DNSSEC protocol. -TrustAnchorFile {{ .Env.OPENDKIM_TRUSTANCHORFILE }} - -# Identifies a set internal hosts whose mail should be signed rather than verified. -InternalHosts {{ .Env.OPENDKIM_INTERNALHOSTS }} \ No newline at end of file