dairyk
dairyk

Reputation: 79

Python flask application access to docker secrets in a swarm

I want to deploy a flask+gunicorn project and I am newbie to Docker. So far, I have a Dockerfile as following.

# Pull official base image
FROM python:3.7-slim-buster

# Set work directory
RUN mkdir -p /usr/src/app
WORKDIR /usr/src/app

# Set environment variables
ENV REDIS_HOST [...omit here...]
ENV REDIS_PORT [...omit here...]
ENV REDIS_DB_WHITELIST [...omit here...]
ENV MYSQL_HOST [...omit here...]
ENV MYSQL_PORT [...omit here...]
ENV MYSQL_DB_DUMMY [...omit here...]

# Copy project
COPY . /usr/src/app/

# Install dependencies
RUN pip install --upgrade pip
RUN pip install -r requirements.txt
RUN pip install gunicorn

EXPOSE 5000

RUN chmod +x ./entrypoint.sh
ENTRYPOINT ["sh", "entrypoint.sh"]

And a docker-compose.yml as following.

version: "3.9"

secrets:
  FLASK_SECRET_KEY:
    external: true
  MYSQL_USER:
    external: true
  MYSQL_PASSWORD:
    external: true

services:
  web:
    image: flask-app:v0.1.0
    environment:
      FLASK_SECRET_KEY_FILE: /run/secrets/FLASK_SECRET_KEY
      MYSQL_USER_FILE: /run/secrets/MYSQL_USER
      MYSQL_PASSWORD_FILE: /run/secrets/MYSQL_PASSWORD
    ports:
      - "5000:5000"
    secrets:
      - FLASK_SECRET_KEY
      - MYSQL_USER
      - MYSQL_PASSWORD

After I googled through, it seems the only way of accessing docker secrets is to use docker stack deploy --compose-file=docker-compose.yml flask-app command. Obviously, I have three sensitive data FLASK_SECRET_KEY, MYSQL_USER, MYSQL_PASSWORD needed to storing in Docker secrets. It turns out that app keeps failing to run, and I assume that mysql_user = os.environ['MYSQL_USER'] etc. in python script fails to access environment variable.

I have no idea of right way to access sensitive data from Docker secrets, via Dockerfile or docker-compose.yml, and please correct me if I get something wrong.

Upvotes: 1

Views: 6184

Answers (2)

dairyk
dairyk

Reputation: 79

I did find an approach of accessing sensitive data from docker secret, using python-dotenv. Here's some snippet of my project-level config.py module.

import os
from dotenv import load_dotenv

dotenv_path = os.path.join(os.path.dirname(__file__), '.env')
if os.path.exists(dotenv_path):
    load_dotenv(dotenv_path=dotenv_path)


def manage_sensitive(name):
    v1 = os.getenv(name)
    
    secret_fpath = f'/run/secrets/{name}'
    existence = os.path.exists(secret_fpath)
    
    if v1 is not None:
        return v1
    
    if existence:
        v2 = open(secret_fpath).read().rstrip('\n')
        return v2
    
    if all([v1 is None, not existence]):
        return KeyError(f'{name}')


class ConfigRabbitMQ:
    AMQP_USER = manage_sensitive(name='amqp_user')
    AMQP_PASSWORD = manage_sensitive(name='amqp_password')
    
    AMQP_HOST = manage_sensitive(name='amqp_host')
    AMQP_PORT = manage_sensitive(name='amqp_port')

So there's a .env file at the same directory with this config.py module. This module can access sensitive data from Docker secret and .env file, as it is a common practice to list .env in a .dockerignore file. Thus, for example, docker-compose.yml is as following.

version: "3.9"

services:
  web:
    ...
    secrets:
      - amqp_user
      - amqp_password
    ...

secrets:
  amqp_user:
    external: true
  amqp_password:
    external: true

Is there's any advice for a better practice ?

Upvotes: 1

Metin
Metin

Reputation: 741

You used the short syntax to declare the secrets on the service. By default such secrets will be mounted in /run/secrets/{secretname} inside the container.

The long syntax even allows to specify the target location (even though the v3 compose reference claims it would only mount to /run/secrets/):

Create a secret:

echo "mysecret" | docker secret create mysecret -

Note: the secret could have been created within a docker-compose.yml with a file reference as well. For simplicity I chose to create it manualy in my example.

Consume the secret in a swarm stack:

version: '3.8'
services:
  testsecret:
    image: ubuntu
    deploy:
      replicas: 1
    tty: true
    secrets:
      - source: mysecret
        target: /path/in/container/mysecret
        mode: 0444

secrets:
  mysecret:
    external: true

Then read the file /path/in/container/mysecret within your application to get its content.

Bare in mind that secrets are always mounted as read-only. The added security is that the secret is destributed encrypted amongst swarm nodes and stored encrypted in the raft log (~=the cluster state). Once a secret is mounted in a container, it will be an unencrypted file on a tempfs. Another advantage is that the details are not exposed as environment variables, thus can less likey by accidentaly leaked.

It seems that docker-compose has a definition gap with the v3.x version of the compose file specs that allows to use secrets with docker-compose deployments as well. But: many low level functionality won't be available then.

Since a coouple of days, the v3 compose file reference page seem to be broken: appart from headers and footers, the whole body of the description is missing...

Upvotes: 0

Related Questions