Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Conversation

@aartoni
Copy link
Contributor

@aartoni aartoni commented Feb 13, 2025

Description

Fixes #3457.

Allows setting any environment variable via the content of a file, this is especially useful to carry Docker secrets in. This fix takes inspiration from a few similar approaches, namely Bitnami container images (e.g., OpenLDAP) and Grafana.

I have marked this as a breaking change since it would change the behavior of setups providing <VAR>__FILE env vars where <VAR> is an existing configuration variable. However, you may consider that to be negligible.

I've noticed a few Fail2ban tests breaking, but I'm not sure if that's due to my local setup or what. I've ignored them as they seemed unrelated.

Type of change

  • Breaking change (fix or feature that would cause existing functionality to not work as expected)
  • This change requires a documentation update

Checklist

  • My code follows the style guidelines of this project
  • I have performed a self-review of my code
  • I have commented my code, particularly in hard-to-understand areas
  • I have made corresponding changes to the documentation (README.md or the documentation under docs/)
  • If necessary, I have added tests that prove my fix is effective or that my feature works
  • New and existing unit tests pass locally with my changes
  • I have added information about changes made in this PR to CHANGELOG.md

@georglauterbach
Copy link
Member

Just a small heads-up: this will not go into v15 anymore, but rather v15.1, because I will start a release for v15 soon.

@polarathene polarathene added this to the v15.1.0 milestone Feb 14, 2025
@polarathene polarathene added kind/new feature A new feature is requested in this issue or implemeted with this PR area/security area/scripts area/documentation area/configuration (file) labels Feb 14, 2025
@polarathene
Copy link
Member

I'll review when DMS v15 is released. Presently trying to tackle various docs PRs within that window until then.

@georglauterbach georglauterbach added the meta/feature freeze On hold due to upcoming release process label Feb 18, 2025
@georglauterbach georglauterbach removed the meta/feature freeze On hold due to upcoming release process label Mar 1, 2025

if [[ -f "${file_path}" ]]; then
_log 'info' "Getting secret ${env_var} from ${file_path}"
export "${env_var}"="$(< "${file_path}")"
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is export needed, what's the purpose?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice catch! Exporting the variables is only needed if we need programs called by the startup script to read them. I expected that to be the case, I'll remove the export if that's not the case!

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I forget the use for export when run within the same scripts, will it carry over to check-for-changes.sh? We seem to rely on getting our ENV from /etc/dms-settings for that (and setup CLI commands):


One of the ENV we explicitly set is BIND_PW (LDAP):

VARS[LDAP_BIND_PW]="${LDAP_BIND_PW:=}"

So this PR would have BIND_PW__FILE set BIND_PW, writing the secret to /etc/dms-settings for anyone in the container to read.

Once the LDAP refactor PR is merged, a future change might have check-for-changes.sh support updating those config files, which would need access to the secret. However we'd not be writing all variations to /etc/dms-settings AFAIK, the feature would work with standard ENV, but not this __FILE variation in that case. Easy enough fix though to call this functionality when initializing check-for-changes.sh.

BIND_PW (technically LDAP_BIND_PW / DOVECOT_BIND_PW) and similar variations supported there.

Copy link
Member

@polarathene polarathene left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

My review feedback is mostly about the feature itself rather than implementation here (I'll leave that to @casperklein and @georglauterbach as they're both better versed with bash than I am).

One concern is the tests don't seem to be verifying the value read from file to ENV is correct.

  • I have seen other projects implement file loaded ENV where a trailing \n or similar was accidentally part of the assigned value.
  • EDIT: While the output to stdout in a shell would include white-space, assignment from the subshell seems to trim the trailing white-space 🤔 (so not affected by this caveat, but should probably still have a test for expected vs actual value)

Could we confirm what the expectation of this feature is security wise?

  • Is it to avoid the container metadata about ENV the container was configured with? (docker inspect <container-name> | jq -r .[].Config.Env / Docker socket API query) Both operations should technically be privileged, but it's not uncommon to provide docker socket access to a container with more access than it needs (even just for labels monitoring I think this would intersect the same API call for environment/env_file on a container - confirmed)
  • Or is the concern with ENV visibility within the container from a non-root user/process? This information is presently publicly accessible at /etc/dms-settings (although I don't think it needs to be 644?), thus not a major improvement.

This PR would only be addressing the ENV metadata on the container from exposing secrets. If internal file access permissions is a concern that'll probably need to be tackled via a separate PR.


Reference

Linking my feedback from the FR issue:

  • #3457 (comment) (FR approved for minor convenience, suggested to use filepath as ENV value instead of __FILE suffix, although the suffix is more useful when we don't have an explicit list)
  • #3457 (comment) (may leak to ENV + written copy to disk)

Verified:

  • Both /root/.bashrc and /etc/dms-settings are written with 644 permissions, but /root itself is 700, thus non-root users can only access /etc/dms-settings.
  • env will have all ENV loaded from /etc/dms-settings when using a bash shell specifically to trigger the .bashrc (docker exec -it bash, or if sh then running a login shell via bash -lc 'env').
  • Running bash scripts via bash (without -l) or relying on the standard shebang we use, neither will load in the ENV, hence why we source it from /etc/dms-settings manually when needed.

My earlier comments in the FR also expressed a bit of uncertainty to the value for this feature in DMS, discouraging it given the intention for security benefit seemed minimal?:

NOTE: In the original FR, I cited Compose secrets as not reliable. There has been recent activity to actually get proper secrets support in Docker Compose with file working with uid / gid / mode (still WIP, nothing landed yet), but that won't really help with the concerns raised for secrets exposure in DMS.


if [[ -f "${file_path}" ]]; then
_log 'info' "Getting secret ${env_var} from ${file_path}"
export "${env_var}"="$(< "${file_path}")"
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I forget the use for export when run within the same scripts, will it carry over to check-for-changes.sh? We seem to rely on getting our ENV from /etc/dms-settings for that (and setup CLI commands):


One of the ENV we explicitly set is BIND_PW (LDAP):

VARS[LDAP_BIND_PW]="${LDAP_BIND_PW:=}"

So this PR would have BIND_PW__FILE set BIND_PW, writing the secret to /etc/dms-settings for anyone in the container to read.

Once the LDAP refactor PR is merged, a future change might have check-for-changes.sh support updating those config files, which would need access to the secret. However we'd not be writing all variations to /etc/dms-settings AFAIK, the feature would work with standard ENV, but not this __FILE variation in that case. Easy enough fix though to call this functionality when initializing check-for-changes.sh.

BIND_PW (technically LDAP_BIND_PW / DOVECOT_BIND_PW) and similar variations supported there.

@casperklein
Copy link
Member

I am not sure if I understand you correctly, but using export makes a variable available to child processes. check-for-changes.sh is started by supervisord. So it's not a child of the start script.

@polarathene
Copy link
Member

polarathene commented Mar 26, 2025

check-for-changes.sh is started by supervisord. So it's not a child of the start script.

Ok, but the source /etc/dms-settings will be run after the start script has finished updating that file yes?

Any ENV not stored in that file will not be usable by check-for-changes.sh or elsewhere after __FILE is processed. Those would only be used via the startup script, so while export may not be necessary, if they're not part of the VARS associative map then they'll not be persisted.

Just highlighting this as a potential caveat where subtle bugs may later appear.


Anything within the container that can read /etc/dms-settings (0644 permissions) will have access to the file-based secret values centralized there.

This feature AFAIK is only to prevent secrets visibility from anyone with access to the Docker socket by sending a GET HTTP request, similar to how the Docker CLI works:

# Read the ENV associated to the running container:
docker inspect <container-name> | jq -r .[].Config.Env

# Shell into DMS as with an interactive TTY as root (implicitly starts a login shell `-l`):
# NOTE: Secrets would be visible in ENV loaded from `/etc/dms-settings` via `/root/.bashrc`
docker exec -it <container-name> bash

I think this feature makes more sense when the secret is read temporarily into memory to use it, but due to how DMS is managing the secrets currently, this is to effectively only defend against a separately compromised container inspecting metadata through the Docker socket.

I just want to clarify with the author (and any others interested in the feature) if that meets their actual expectations of implementing this feature, or if it falls short of the expected security benefit.

If @aartoni still wants to proceed with that security benefit clarified, then they just need to ensure the PR has a test-case that verifies the target ENV has the expected value resolved by the __FILE variant.

@aartoni
Copy link
Contributor Author

aartoni commented Mar 27, 2025

@polarathene I'm willing to proceed. I'll get in touch in a few days in case I get stuck.

@github-actions

This comment was marked as off-topic.

@github-actions github-actions bot added the meta/stale This issue / PR has become stale and will be closed if there is no further activity label Apr 17, 2025
@aartoni
Copy link
Contributor Author

aartoni commented Apr 20, 2025

Hello @polarathene! I got stuck while trying to address this comment.

It is not clear to me how to obtain the path for the copy of the configuration that I should be using in this case, here's what I have tried:

TEST_TMP_CONFIG=$(_duplicate_config_for_container . 'env_vars_from_files')

@github-actions github-actions bot removed the meta/stale This issue / PR has become stale and will be closed if there is no further activity label Apr 21, 2025
@polarathene
Copy link
Member

We have a variety of tests you can reference that do this :)

TEST_TMP_CONFIG should be already managed for you (duplicated for each container configured in setup_file()), so like the examples below, you can use mv or cp to place files where you need 👍

If you need to create files instead of cp/mv, you should be able to create files at the TEST_TMP_CONFIG location too. Just make sure you do this after calling _init_with_defaults first (configures TEST_TMP_CONFIG for the test) and before calling _common_container_setup (starts the container with TEST_TMP_CONFIG volume mounted).

Reference of tests manipulating configs from `TEST_TMP_CONFIG` (click to view)

# Move sieve configs into main `/tmp/docker-mailserver` config location:
mv "${TEST_TMP_CONFIG}/dovecot-sieve/"* "${TEST_TMP_CONFIG}/"

# Move override configs into main `/tmp/docker-mailserver` config location:
mv "${TEST_TMP_CONFIG}/override-configs/"* "${TEST_TMP_CONFIG}/"

CONTAINER_NAME=${CONTAINER1_NAME}
local CUSTOM_SETUP_ARGUMENTS=(
--env ENABLE_FETCHMAIL=1
)
_init_with_defaults
mv "${TEST_TMP_CONFIG}/fetchmail/fetchmail.cf" "${TEST_TMP_CONFIG}/fetchmail.cf"
_common_container_setup 'CUSTOM_SETUP_ARGUMENTS'

_init_with_defaults
local CONTAINER_ARGS_ENV_CUSTOM=(
--env PERMIT_DOCKER='container'
--env POSTFIX_DAGENT="${LMTP_URI}"
)
# Configure LMTP service listener in `/etc/dovecot/conf.d/10-master.conf` to instead listen on TCP port 24:
mv "${TEST_TMP_CONFIG}/dovecot-lmtp/user-patches.sh" "${TEST_TMP_CONFIG}/"
_common_container_setup 'CONTAINER_ARGS_ENV_CUSTOM'

export CONTAINER_NAME=${CONTAINER1_NAME}
_init_with_defaults
# Unset `smtpd_discard_ehlo_keywords` to allow DSNs by default on any `smtpd` service:
cp "${TEST_TMP_CONFIG}/dsn/postfix-main.cf" "${TEST_TMP_CONFIG}/postfix-main.cf"
_common_container_setup 'CUSTOM_SETUP_ARGUMENTS'
_wait_for_service postfix
_wait_for_smtp_port_in_container

# Required for 'delivers mail to existing alias with recipient delimiter':
mv "${TEST_TMP_CONFIG}/smtp-delivery/postfix-main.cf" "${TEST_TMP_CONFIG}/postfix-main.cf"
mv "${TEST_TMP_CONFIG}/smtp-delivery/dovecot.cf" "${TEST_TMP_CONFIG}/dovecot.cf"
_common_container_setup 'CONTAINER_ARGS_ENV_CUSTOM'

# Initializes common default vars to prepare a DMS container with:
_init_with_defaults
mv "${TEST_TMP_CONFIG}/fetchmail/fetchmail.cf" "${TEST_TMP_CONFIG}/fetchmail.cf"

function setup_file() {
export TMP_CONFIG_FILE=$(mktemp)
cp "${REPOSITORY_ROOT}/test/config/override-configs/replace_by_env_in_file.conf" "${TMP_CONFIG_FILE}"
}
function teardown_file() { rm "${TMP_CONFIG_FILE}" ; }


Extra notes

  • If you need to have files provided already, you can commit them to this directory which is the contents of TEST_TMP_CONFIG (duplicated per container setup).

  • _init_with_defaults() duplicates the test config directory for you setting it to TEST_TMP_CONFIG:

    export TEST_TMP_CONFIG
    TEST_TMP_CONFIG=$(_duplicate_config_for_container . "${CONTAINER_NAME}")

    TEST_TMP_CONFIG is then added as a volume mount for the container to use as /tmp/docker-mailserver (DMS Config Volume):

    export TEST_CONFIG_VOLUME="${TEST_TMP_CONFIG}:/tmp/docker-mailserver"

    --volume "${TEST_CONFIG_VOLUME}" \

  • We also document here how you can create a separate TEST_TMP_CONFIG on demand when needed (see the example portion):

    # Common defaults appropriate for most tests.
    #
    # Override variables in test cases within a file when necessary:
    # - Use `export <VARIABLE>` in `setup_file()` to overrides for all test cases.
    # - Use `local <VARIABLE>` to override within a specific test case.
    #
    # ## Attenton
    #
    # The ENV `CONTAINER_NAME` must be set before this method is called. It only affects the
    # `TEST_TMP_CONFIG` directory created, but will be used in `common_container_create()`
    # and implicitly in other helper methods.
    #
    # ## Example
    #
    # For example, if you need an immutable config volume that can't be affected by other tests
    # in the file, then use `local TEST_TMP_CONFIG=$(_duplicate_config_for_container . "${UNIQUE_ID_HERE}")`

  • Automated removal happens here:

    # `docker ps`: Remove any lingering test containers
    # `.gitignore`: Remove `test/duplicate_configs` and files copied via `make generate-accounts`
    clean: ALWAYS_RUN
    -@ while read -r LINE; do CONTAINERS+=("$${LINE}"); done < <(docker ps -qaf name='^(dms-test|mail)_.*') ; \
    for CONTAINER in "$${CONTAINERS[@]}"; do docker rm -f "$${CONTAINER}"; done
    -@ while read -r LINE; do [[ $${LINE} =~ test/.+ ]] && FILES+=("/mnt$${LINE#test}"); done < .gitignore ; \
    docker run --rm -v "$(REPOSITORY_ROOT)/test/:/mnt" alpine ash -c "rm -rf $${FILES[@]}"

    Which will remove all the duplicated config folders tests created at test/duplicate_configs/:

    test/duplicate_configs/

@github-actions github-actions bot added the meta/stale This issue / PR has become stale and will be closed if there is no further activity label May 12, 2025
@aartoni
Copy link
Contributor Author

aartoni commented May 12, 2025

Thank you @polarathene for referencing the relevant code. I have implemented the tests the way you pointed out, I'd say that this is ready for the final review!

@github-actions github-actions bot removed the meta/stale This issue / PR has become stale and will be closed if there is no further activity label May 13, 2025
polarathene
polarathene previously approved these changes May 14, 2025
Copy link
Member

@polarathene polarathene left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM 👍 Thanks for taking the time to contribute this feature! ❤️

Just waiting on @casperklein or @georglauterbach for their opinion on #4359 (comment) (there is also earlier review discussion for this too but for export at #4359 (comment))

Copy link
Member

@georglauterbach georglauterbach left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I prefer the local -n approach outlined in my suggestion. I hope you agree with my idea :) Please double-check it, though :) Other than that: LGTM, nice addition 👍🏼

@aartoni
Copy link
Contributor Author

aartoni commented May 15, 2025

I'm sorry @polarathene, I have unwillingly dismissed your review, can you please contribute back the comments so that I can approve them? Or simply push without my help if you prefer :)

@casperklein
Copy link
Member

I'll review this on the weekend.

@github-actions
Copy link
Contributor

Documentation preview for this PR is ready! 🎉

Built with commit: 9ef06e6

@casperklein casperklein merged commit 53c3619 into docker-mailserver:master May 17, 2025
9 checks passed
@aartoni aartoni deleted the feat/env-vars-from-files branch May 21, 2025 12:06
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area/configuration (file) area/documentation area/scripts area/security kind/new feature A new feature is requested in this issue or implemeted with this PR

Projects

None yet

Development

Successfully merging this pull request may close these issues.

feature request: Adding LDAP_BIND_PW_FILE to support docker secrets

4 participants