Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Adds support for loading environment vars from file. #938

Merged
merged 2 commits into from
Dec 5, 2023

Conversation

MicahZoltu
Copy link
Contributor

The docker entrypoint script appears to look for secrets (along with other things) in the environment. Providing secrets via environment variables has a couple of negative security implications:

  1. Anyone who can see your docker compose/swarm file can see your secrets.
  2. Anyone who can inspect your docker environment can see your secrets (e.g., docker ps)

Docker swarm supports the concept of Docker Secrets, which are defined and managed by docker swarm with their own set of access controls independent from access to to management and compose files. When one uses docker secrets, they get mounted in a tmpfs drive inside the container at /run/secrets/* (where * is the name of the secret defined in the compose/swarm file). These secrets can be simple one-line files (e.g., a password) or complex files (e.g., a TLS key).

By having the entrypoint script load environment variables from a provided file path like this, we can securely move the secrets into the container and load them without ever exposing them outside of the container.

The docker entrypoint script appears to look for secrets (along with other things) in the environment.  Providing secrets via environment variables has a couple of negative security implications:
1. Anyone who can see your docker compose/swarm file can see your secrets.
2. Anyone who can inspect your docker environment can see your secrets (e.g., `docker ps`)

Docker swarm supports the concept of Docker Secrets, which are defined and managed by docker swarm with their own set of access controls independent from access to to management and compose files.  When one uses docker secrets, they get mounted in a `tmpfs` drive inside the container at `/run/secrets/*` (where `*` is the name of the secret defined in the compose/swarm file).  These secrets can be simple one-line files (e.g., a password) or complex files (e.g., a TLS key).

By having the entrypoint script load environment variables from a provided file path like this, we can securely move the secrets into the container and load them without ever exposing them outside of the container.
@MicahZoltu
Copy link
Contributor Author

I did my best to follow conventions found in this file, but I could have gotten something wrong so just let me know if I need to make changes.

The variable name for ENVIRONMENT_FILE is essentially a placeholder, I'm happy to change it to whatever you all prefer.

@moughxyz moughxyz requested a review from karolsojko November 27, 2023 14:28
@karolsojko
Copy link
Member

@MicahZoltu defining a path to a file that is dynamically loaded exposes a different vector of security vulnerabilities.

In docker-compose you can define you env file, f.e.: https://github.com/standardnotes/server/blob/main/docker-compose.example.yml#L4

In Docker Swarm to my understanding you can define the secrets that will be passed to the service. The standardnotes/server image is fully capable of handling you env variables whatever way you have them defined.

The docker-entrypoint.sh script is just for interpreting the variables that are set in the environment on which the image is running and passing them to the respective microservices.

Setting env vars from a file is not in the scope of standardnotes server.

@MicahZoltu
Copy link
Contributor Author

@MicahZoltu defining a path to a file that is dynamically loaded exposes a different vector of security vulnerabilities.

What security vulnerabilities are you thinking of here? The Docker documentation explicitly describes these secrets files as the secure way to pass runtime secrets to a container, and modern images support this in various ways (for example, MySQL supports DB_PASSWORD_FILE which is a path to a file that has a single string in it which is the password).

In docker-compose you can define you env file, f.e.: https://github.com/standardnotes/server/blob/main/docker-compose.example.yml#L4

.env files in Docker Compose and Docker Swarm are not secure as I understand it. All it does is pass the environment variables contained in the file to the docker container from the outside, the same as defining them in the environment section of the docker-compose.yml. This does solve the problem of someone having read access to the compose file, but it doesn't fully solve the problem of secrets management because you still need to deal with the visibility of those secrets by anyone with view access to the host, as well as visibility of the .env file (which I don't believe can be a Docker Secret).

In Docker Swarm to my understanding you can define the secrets that will be passed to the service. The standardnotes/server image is fully capable of handling you env variables whatever way you have them defined.

Docker secrets get mounted as a file inside the container. There is no way that I know of (and I looked into this before opening this issue) to pass secrets as environment variables to the root process inside the container directly. It is up to the container to turn the file in /run/secrets into environment variables inside the container if that is necessary (as I believe is the case here).

You can get more information about how Docker Swarm Secrets work at https://docs.docker.com/engine/swarm/secrets/#how-docker-manages-secrets, or if you just want some examples (including the MySQL example I referenced above) you can check out https://docs.docker.com/compose/use-secrets/

Setting env vars from a file is not in the scope of standardnotes server.

I have been trying to find an alternative workaround that is secure, but unfortunately the best I have come up with is to build my own derivative image based on your image that has this code prepended to your base image. I would much rather see this integrated upstream, as that makes it so people don't have to trust some rando on the internet to provide a docker image that allows for secure secret storage.

@karolsojko
Copy link
Member

Hmm correct if I'm wrong but dynamically loading the variables would not make them secret. After this part is executed:

  export $(cat "$ENVIRONMENT_FILE" | xargs)

if I have view access to the host as you mentioned I would still be able to get the env vars of that container.

I understand your problem but I think this calls for a much more complex solution, meaning something like this (this is just a pseudo solution to give the idea):

services:
  myapp:
    image: standardnotes/server
    secrets:
      - my_secret
secrets:
  my_secret:
    file: ./my_secret.txt

And in my_secret.txt you'd have to define all the env vars that are mentioned in docker-entrypoint.sh - would that work for you?

@MicahZoltu
Copy link
Contributor Author

Hmm correct if I'm wrong but dynamically loading the variables would not make them secret. After this part is executed:

export $(cat "$ENVIRONMENT_FILE" | xargs)

if I have view access to the host as you mentioned I would still be able to get the env vars of that container.

I believe that is incorrect. If you have view access to the host but do not have the ability to exec into the container, then you would not be able to see those environment variables. The key is that this is executed inside the running container, not on the host.

This is what I had in mind for what the docker-compose.yml would look like after this PR was merged:

services:
  myapp:
    image: standardnotes/server
    environment:
      ENVIRONMENT_FILE: '/run/secrets/my_secret'
    secrets:
      - my_secret
secrets:
  my_secret:
    file: ./my_secret.txt

The container would start up and inside of it is /run/secrets/my_secret. /run/secrets/my_secret would contain the secret environment variables. These variables would be read into process 1's environment, and when supervisord starts at the end of the script it would get those environment variables from its parent process (the entrypoint script).

you'd have to define all the env vars that are mentioned in docker-entrypoint.sh

I would rather only provide overrides in the secrets file, and bring everything else in via environment variables the "normal" way. However, if you have a solution in mind that is would require all configuration to come in through a single file then that would address my immediate problem. It is a tad inconvenient because it means anyone who needs to make any configuration changes must have access to the secrets as well, but in my case that person is the me in both cases since this is for personal use. 😄

That being said, it is unclear to me how you would achieve that other than the way I have proposed here (which allows for only providing what you want via secrets file). I'm certainly open to other ideas though!

@karolsojko
Copy link
Member

Ok I understand now what you're trying to achieve here, but the export of the entire file is a bit too generous also in terms of security vulnerability.

From what I can see in MySQL they are exporting one secret per file which makes more sense from the predictability point of view: docker-library/mysql@4dd3313#diff-7b63e1c368f06fe2cc9d89949a98742384d920adb7ddcf24f7348d95972d7205R21-R42

This is also mentioned in https://docs.docker.com/compose/use-secrets/ on the _FILE convention:

The _FILE environment variables demonstrated here are a convention used by some images, including Docker Official Images like [mysql](https://hub.docker.com/_/mysql) and [postgres](https://hub.docker.com/_/postgres).

I would suggest you amend your code to that kind of approach instead.

@MicahZoltu
Copy link
Contributor Author

Sure, I can switch this over to the one-per-file strategy. Should there be a generalized mechanism for providing any environment variable as either a _FILE or regular, or should we just have a handful of one-offs for the secrets? I'm not a bash wizard, but a generalized solution doesn't feel too difficult to put together in bash if that is what is desired.

@karolsojko
Copy link
Member

Well a generic approach would be preferred - you can take a look at the mysql one as an example

Copied the technique used by MySQL as seen in docker-library/mysql@4dd3313#diff-7b63e1c368f06fe2cc9d89949a98742384d920adb7ddcf24f7348d95972d7205R21-R42

This makes all usernames/passwords and similar overrideable with a `_FILE` suffix, while leaving everything else as-is for setting via normal environment variables.
@MicahZoltu
Copy link
Contributor Author

@karolsojko I switched to using the technique that MySQL uses, and applied it only to usernames/passwords and key IDs/secrets. Let me know if this aligns with what you had in mind.

@karolsojko
Copy link
Member

Thank you for the contribution @MicahZoltu 🎉

@karolsojko karolsojko merged commit 6bdb524 into standardnotes:main Dec 5, 2023
15 checks passed
@MicahZoltu MicahZoltu deleted the patch-1 branch December 5, 2023 12:50
@moughxyz
Copy link
Member

moughxyz commented Dec 5, 2023

@MicahZoltu would you be able to provide a markdown blurb of how to use this new addition, so we could add it to our docs?

@MicahZoltu
Copy link
Contributor Author

Here is a quick writeup, could probably use some love by someone with better writing skills than I.

(optional) Usernames, passwords, API keys, and API IDs can be provided as files instead of environment variables, which allows leveraging [Docker Secrets](https://docs.docker.com/engine/swarm/secrets/).  To do this, set an environment variable with a name matching the normal environment variable, but with `_FILE` suffix, to a path to the file that contains the secret (as UTF-8 text with no newline).

To utilize this with Docker Swarm Secrets, create a secret with `docker secret create standardnotes_db_password Passw0rd1!` and then reference `standardnotes_db_password` in your Docker Compose file like in the example below.
```yml
services:
  server:
    image: standardnotes/server
    container_name: server_self_hosted
    restart: unless-stopped
    ports: ...
    volumes: ...
    networks: ...
    env_file: .env
    environment:
      - DB_PASSWORD_FILE: '/run/secrets/db_password'
    secrets:
      - db_password
  ...
...
secrets:
  standardnotes_db_password:
    name: db_password
    external: true
```

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