Add the option to log to rsyslog as plain or JSON and restructure tests

- It's now possible to choose the logging type - either 'plain' or
  'json'
- The code is ready to support multiple integration tests (with
  different configurations)
- `OPENDKIM_` and `POSTFIX_` variables are handled properly and recorded
  in the corresponding files. (This had a downfall that `bash` now needs
  to be installed, so we can probably simplify some of the shell
  scripts.)
This commit is contained in:
Bojan Čekrlić 2020-07-01 13:50:08 +02:00
parent 9b1902c047
commit ff2d080279
17 changed files with 211 additions and 17 deletions

View file

@ -29,6 +29,8 @@ ENV MESSAGE_SIZE_LIMIT=
ENV INBOUND_DEBUGGING= ENV INBOUND_DEBUGGING=
# DKIM domain selector. If not set, the default (mail) will be used # DKIM domain selector. If not set, the default (mail) will be used
ENV DKIM_SELECTOR= ENV DKIM_SELECTOR=
# Logformat. Defaults to "plain". Can be either "plain" or "json".
ENV LOG_FORMAT=
# Install supervisor, postfix # Install supervisor, postfix
# Install postfix first to get the first account (101) # Install postfix first to get the first account (101)
@ -37,13 +39,12 @@ RUN true && \
apk add --no-cache --upgrade cyrus-sasl cyrus-sasl-plain cyrus-sasl-login && \ apk add --no-cache --upgrade cyrus-sasl cyrus-sasl-plain cyrus-sasl-login && \
apk add --no-cache postfix && \ apk add --no-cache postfix && \
apk add --no-cache opendkim && \ apk add --no-cache opendkim && \
apk add --no-cache ca-certificates tzdata supervisor rsyslog && \ apk add --no-cache --upgrade ca-certificates tzdata supervisor rsyslog musl musl-utils bash && \
apk add --no-cache --upgrade musl musl-utils && \
(rm "/tmp/"* 2>/dev/null || true) && (rm -rf /var/cache/apk/* 2>/dev/null || true) (rm "/tmp/"* 2>/dev/null || true) && (rm -rf /var/cache/apk/* 2>/dev/null || true)
# Set up configuration # Set up configuration
COPY /configs/supervisord.conf /etc/supervisord.conf COPY /configs/supervisord.conf /etc/supervisord.conf
COPY /configs/rsyslog.conf /etc/rsyslog.conf COPY /configs/rsyslog*.conf /etc/
COPY /configs/opendkim.conf /etc/opendkim/opendkim.conf COPY /configs/opendkim.conf /etc/opendkim/opendkim.conf
COPY /configs/smtp_header_checks /etc/postfix/smtp_header_checks COPY /configs/smtp_header_checks /etc/postfix/smtp_header_checks
COPY /scripts/*.sh / COPY /scripts/*.sh /

View file

@ -219,6 +219,8 @@ This means:
## Extending the image ## Extending the image
### Using custom init scripts
If you need to add custom configuration to postfix or have it do something outside of the scope of this configuration, simply If you need to add custom configuration to postfix or have it do something outside of the scope of this configuration, simply
add your scripts to `/docker-init.db/`: All files with the `.sh` extension will be executed automatically at the end of the add your scripts to `/docker-init.db/`: All files with the `.sh` extension will be executed automatically at the end of the
startup script. startup script.
@ -243,7 +245,37 @@ For example, your script could contain something like this:
postconf -e "address_verify_negative_cache=yes" postconf -e "address_verify_negative_cache=yes"
``` ```
### Overriding specific postfix settings
Any Postfix [configuration option](http://www.postfix.org/postconf.5.html) can be overriden using `POSTFIX_<name>` environment variables, e.g.
`POSTFIX_allow_mail_to_commands=alias,forward,include`. Specifying no content (empty variable) will remove that variable from postfix config.
Any OpenDKIM [configuration option](http://opendkim.org/opendkim.conf.5.html) can be overriden using `OPENDKIM_<name>` environment variables, e.g.
`OPENDKIM_RequireSafeKeys=yes`. Specifying no content (empty variable) will remove that variable from OpenDKIM config.
## Log format
The image will by default output logs in human-readable (`plain`) format. If you are deploying the image to Kubernetes, it might be worth chaging
the output format to `json` as it's more easily parsable by tools such as [Prometheus](https://prometheus.io/).
To change the log format, set the (unsuprisingly named) variable `LOG_FORMAT=json`.
## Security ## Security
Postfix will run the master proces as `root`, because that's how it's designed. Subprocesses will run under the `postfix` account Postfix will run the master proces as `root`, because that's how it's designed. Subprocesses will run under the `postfix` account
which will use `UID:GID` of `100:101`. `opendkim` will run under account `102:103`. which will use `UID:GID` of `100:101`. `opendkim` will run under account `102:103`.
## Similar projects
There are may other project offering similar functionality. The aim of this project, however, is:
- to make it as simple as possible to run the relay, without going too much into postfix configuration details
- to make as small image as possible (hence basing on Alpine linux)
- to make the image and the corresponding code testable
The other projects are, in completely random order:
- [wader/postfix-relay](https://github.com/wader/postfix-relay)
- [catatnight/postfix](https://github.com/catatnight/docker-postfix)
- [juanluisbaptiste/docker-postfix](https://github.com/juanluisbaptiste/docker-postfix)
- [docker-mail-relay](https://github.com/alterrebe/docker-mail-relay)

View file

@ -10,8 +10,7 @@ $Umask 0022
template (name="devicelog" type="string" string="/dev/stdout") template (name="devicelog" type="string" string="/dev/stdout")
template(name="json_syslog" template(name="json" type="list") {
type="list") {
constant(value="{") constant(value="{")
constant(value="\"@timestamp\":\"") property(name="timereported" dateFormat="rfc3339") constant(value="\"@timestamp\":\"") property(name="timereported" dateFormat="rfc3339")
constant(value="\",\"type\":\"syslog_json") constant(value="\",\"type\":\"syslog_json")
@ -26,17 +25,29 @@ template(name="json_syslog"
constant(value="\",\"facility\":\"") property(name="syslogfacility") constant(value="\",\"facility\":\"") property(name="syslogfacility")
constant(value="\",\"severity_label\":\"") property(name="syslogseverity-text") constant(value="\",\"severity_label\":\"") property(name="syslogseverity-text")
constant(value="\",\"facility_label\":\"") property(name="syslogfacility-text") constant(value="\",\"facility_label\":\"") property(name="syslogfacility-text")
constant(value="\",\"message\":\"") property(name="rawmsg" format="json") constant(value="\",\"message\":\"") property(name="msg" format="json")
constant(value="\",\"end_msg\":\"") constant(value="\",\"end_msg\":\"")
constant(value="\"}\n") constant(value="\"}\n")
} }
template(name="plain" type="list") {
property(name="timereported" dateFormat="rfc3339")
constant(value=" ")
property(name="syslogseverity-text" caseconversion="upper" fixedwidth="on" position.to="7")
constant(value=" ")
property(name="syslogtag")
property(name="msg" spifno1stsp="on")
property(name="msg" droplastlf="on")
constant(value="\n")
}
if $syslogseverity <= '6' then { if $syslogseverity <= '6' then {
# matching logs will be saved # matching logs will be saved
action(type="omfile" DynaFile="devicelog" template="json_syslog" DirCreateMode="0755" FileCreateMode="0644") action(type="omfile" DynaFile="devicelog" template="<log-format>" DirCreateMode="0755" FileCreateMode="0644")
# enable below to stop processing further this log # enable below to stop processing further this log
stop stop
} }
include(file="/etc/rsyslog.d/*.conf" mode="optional")
stop stop

View file

@ -1,3 +1,14 @@
#!/bin/sh #!/bin/sh
set -e
cd integration-tests cd integration-tests
docker-compose up --build --abort-on-container-exit --exit-code-from tests for i in `find -maxdepth 1 -type d`; do
i="$(basename "$i")"
if [ "$i" == "tester" ] || [ "$i" == "." ] || [ "$i" == ".." ]; then
continue
fi
(
echo "$i"
cd "$i"
docker-compose up --build --abort-on-container-exit --exit-code-from tests
)
done

View file

@ -4,7 +4,7 @@ services:
hostname: "postfix" hostname: "postfix"
image: "boky/postfix" image: "boky/postfix"
build: build:
context: .. context: ../..
restart: always restart: always
healthcheck: healthcheck:
test: [ "CMD", "sh", "-c", "netstat -an | fgrep 587 | fgrep -q LISTEN" ] test: [ "CMD", "sh", "-c", "netstat -an | fgrep 587 | fgrep -q LISTEN" ]
@ -21,7 +21,7 @@ services:
image: "boky/postfix-integration-test" image: "boky/postfix-integration-test"
restart: "no" restart: "no"
volumes: volumes:
- "..:/code" - "../tester:/code"
build: build:
context: . context: ../tester
command: "/code/integration-tests/" command: "/"

View file

@ -12,7 +12,6 @@ if [ -z "$TO" ]; then
fi fi
# Wait for postfix to startup # Wait for postfix to startup
echo "Waiting for startup..."
wait-for-service -q tcp://postfix_test_587:587 wait-for-service -q tcp://postfix_test_587:587
SMTP_DATA="-smtp postfix_test_587 -port 587" SMTP_DATA="-smtp postfix_test_587 -port 587"

View file

@ -1,4 +1,4 @@
#!/bin/sh #!/usr/bin/env bash
announce_startup() { announce_startup() {
echo -e "******************************" echo -e "******************************"
@ -21,6 +21,15 @@ setup_timezone() {
fi fi
} }
rsyslog_log_format() {
local log_format="${LOG_FORMAT}"
if [[ -z "${log_format}" ]]; then
log_format="plain"
fi
echo -e "$info Using ${emphasis}${log_format}${reset} log format for rsyslog."
sed -i -E "s/<log-format>/${log_format}/" /etc/rsyslog.conf
}
reown_folders() { reown_folders() {
mkdir -p /var/spool/postfix/ && mkdir -p /var/spool/postfix/pid mkdir -p /var/spool/postfix/ && mkdir -p /var/spool/postfix/pid
chown root: /var/spool/postfix/ chown root: /var/spool/postfix/
@ -250,6 +259,52 @@ postfix_setup_dkim() {
fi fi
} }
opendkim_custom_commands() {
local setting
local key
local padded_key
local value
for setting in ${!OPENDKIM_*}; do
key="${setting:9}"
value="${!setting}"
if [ -n "${value}" ]; then
if [ "${#key}" -gt 23 ]; then
padded_key="${key} "
else
padded_key="$(printf %-24s "${key}")"
fi
if cat /etc/opendkim/opendkim.conf | egrep -q "^[[:space:]]*#?[[:space:]]*${key}"; then
echo -e "$info Updating custom OpenDKIM setting: ${emphasis}${key}=${value}${reset}"
sed -i -E "s/^[ \t]*#?[ \t]*${key}[ \t]*.+$/${padded_key}${value}/" /etc/opendkim/opendkim.conf
else
echo -e "$info Adding custom OpenDKIM setting: ${emphasis}${key}=${value}${reset}"
echo "Adding ${padded_key}${value}"
echo "${padded_key}${value}" >> /etc/opendkim/opendkim.conf
fi
else
echo -e "$info Deleting custom OpenDKIM setting: ${emphasis}${key}${reset}"
sed -i -E "/^[ \t]*#?[ \t]*${key}[ \t]*.+$/d" /etc/opendkim/opendkim.conf
fi
done
}
postfix_custom_commands() {
local setting
local key
local value
for setting in ${!POSTFIX_*}; do
key="${setting:8}"
value="${!setting}"
if [ -n "${value}" ]; then
echo -e "$info Applying custom postfix setting: ${emphasis}${key}=${value}${reset}"
postconf -e "${key}=${value}"
else
echo -e "$info Deleting custom postfix setting: ${emphasis}${key}${reset}"
postconf -# "${key}"
fi
done
}
postfix_open_submission_port() { postfix_open_submission_port() {
# Use 587 (submission) # Use 587 (submission)
sed -i -r -e 's/^#submission/submission/' /etc/postfix/master.cf sed -i -r -e 's/^#submission/submission/' /etc/postfix/master.cf

View file

@ -1,4 +1,4 @@
#!/bin/sh #!/usr/bin/env bash
reset="" reset=""
yellow="" yellow=""

View file

@ -1,4 +1,4 @@
#!/bin/sh #!/usr/bin/env bash
set -e set -e
. /common.sh . /common.sh
@ -6,6 +6,7 @@ set -e
announce_startup # Print startup banner announce_startup # Print startup banner
setup_timezone # Check if we need to configure the container timezone setup_timezone # Check if we need to configure the container timezone
rsyslog_log_format # Setup rsyslog output format
reown_folders # Make and reown postfix folders reown_folders # Make and reown postfix folders
postfix_disable_utf8 # Disable SMTPUTF8, because libraries (ICU) are missing in alpine postfix_disable_utf8 # Disable SMTPUTF8, because libraries (ICU) are missing in alpine
postfix_create_aliases # Update aliases database. It's not used, but postfix complains if the .db file is missing postfix_create_aliases # Update aliases database. It's not used, but postfix complains if the .db file is missing
@ -23,6 +24,8 @@ postfix_setup_sender_domains # Configure allowed sender domains
postfix_setup_masquarading # Setup masquaraded domains postfix_setup_masquarading # Setup masquaraded domains
postfix_setup_header_checks # Enable SMTP header checks, if defined postfix_setup_header_checks # Enable SMTP header checks, if defined
postfix_setup_dkim # Configure DKIM, if enabled postfix_setup_dkim # Configure DKIM, if enabled
postfix_custom_commands # Apply custom postfix settings
opendkim_custom_commands # Apply custom OpenDKIM settings
postfix_open_submission_port # Enable the submission port postfix_open_submission_port # Enable the submission port
execute_post_init_scripts # Execute any scripts found in /docker-init.db/ execute_post_init_scripts # Execute any scripts found in /docker-init.db/

View file

@ -1,7 +1,12 @@
FROM alpine:latest FROM alpine:latest
RUN apk add --no-cache bash bats RUN true && \
apk add --no-cache bash bats && \
apk add --no-cache --upgrade cyrus-sasl cyrus-sasl-plain cyrus-sasl-login && \
apk add --no-cache postfix && \
apk add --no-cache opendkim && \
(rm "/tmp/"* 2>/dev/null || true) && (rm -rf /var/cache/apk/* 2>/dev/null || true)
WORKDIR /code WORKDIR /code
ENTRYPOINT ["/usr/bin/bats"] ENTRYPOINT ["/usr/bin/bats"]

View file

@ -0,0 +1,60 @@
#!/usr/bin/env bats
load /code/scripts/common.sh
load /code/scripts/common-run.sh
setup() {
mkdir -p /etc/opendkim/
cat > /etc/opendkim/opendkim.conf <<-EOF
AutoRestart Yes
AutoRestartRate 10/1h
UMask 002
Syslog Yes
SyslogSuccess Yes
LogWhy No
Canonicalization relaxed/simple
RequireSafeKeys no
ExternalIgnoreList refile:/etc/opendkim/TrustedHosts
InternalHosts refile:/etc/opendkim/TrustedHosts
KeyTable refile:/etc/opendkim/KeyTable
SigningTable refile:/etc/opendkim/SigningTable
Mode sv
PidFile /var/run/opendkim/opendkim.pid
SignatureAlgorithm rsa-sha256
UserID opendkim:opendkim
Socket inet:8891@localhost
SignHeaders From,Sender,To,CC,Subject,Message-Id,Date,MIME-Version,Content-Type,Reply-To
OversignHeaders From,Sender,To,CC,Subject,Message-Id,Date,MIME-Version,Content-Type,Reply-To
EOF
}
teardown() {
rm -f /etc/opendkim/opendkim.conf
}
@test "Make sure that opendkim_custom_commands changes lines" {
local OPENDKIM_RequireSafeKeys=yes
opendkim_custom_commands
cat /etc/opendkim/opendkim.conf | fgrep -qx "RequireSafeKeys yes"
}
@test "Make sure that opendkim_custom_commands adds lines" {
local OPENDKIM_CaptureUnknownErrors=yes
opendkim_custom_commands
cat /etc/opendkim/opendkim.conf | fgrep -qx "CaptureUnknownErrors yes"
}
@test "Make sure that opendkim_custom_commands removes lines" {
local OPENDKIM_SignHeaders=
opendkim_custom_commands
if cat /etc/opendkim/opendkim.conf | egrep -q "^SignHeaders"; then
return 1
fi
}

View file

@ -0,0 +1,17 @@
#!/usr/bin/env bats
load /code/scripts/common.sh
load /code/scripts/common-run.sh
@test "Make sure that postfix_custom_commands adds lines" {
local POSTFIX_alias_database=hash:/etc/mail/aliases
postfix_custom_commands
cat /etc/postfix/main.cf | fgrep -qx "alias_database = hash:/etc/mail/aliases"
}
@test "Make sure that postfix_custom_commands removes lines" {
local POSTFIX_readme_directory=
postfix_custom_commands
cat /etc/postfix/main.cf | egrep -q "^#readme_directory"
}