Production Secrets Management in OpenTelemetry .NET

In OpenTelemetry .NET, secrets management in production environment is crucial to ensure the security and integrity of sensitive information. This documentation outlines the strategy for storing and managing secrets when using OpenTelemetry with Docker in a production setup.

Environment Variables

A common practice to manage secrets in production is through the use of environment variables. This is employed in the docker-compose.yml file, where various services are defined, including the web API and worker service. The sensitive information such as service credentials can be injected into containers at runtime via environment variables, making them accessible to the applications without hardcoding them.

services:
  webapi:
    environment:
      - ASPNETCORE_ENVIRONMENT=Development
      - RABBITMQ_HOSTNAME=rabbitmq
      - RABBITMQ_DEFAULT_USER=guest
      - RABBITMQ_DEFAULT_PASS=guest
      - ZIPKIN_HOSTNAME=zipkin

For production environments, it is advisable to leverage secrets management tools or systems to populate these variables securely. For example, you can utilize tools like Docker Secrets, Kubernetes Secrets, or Azure Key Vault, depending on your infrastructure.

Using Docker Secrets

If deploying your application with Docker Swarm, Docker Secrets can be used to manage sensitive data. It’s essential to create secrets that can be referenced in the docker-compose.yml. An example of creating and using a secret for RabbitMQ credentials can be like so:

  1. Create the Secret:

    echo "my_secure_password" | docker secret create rabbitmq_secret -
    
  2. Reference it in the docker-compose.yml:

    services:
      webapi:
        secrets:
          - rabbitmq_secret
        
    secrets:
      rabbitmq_secret:
        external: true
    
  3. Access it in your Application: Inside your application, you can access the secrets typically from the file system at /run/secrets/rabbitmq_secret.

Configuration Files and Jinja Templates

For more complex configurations, sometimes Jinja templates can be utilized to dynamically populate configuration settings. This is especially useful when you need to substitute sensitive information in configuration files.

Example of a Jinja template for configuration:

RABBITMQ_DEFAULT_USER={{ lookup('env', 'RABBITMQ_USER') }}
RABBITMQ_DEFAULT_PASS={{ lookup('env', 'RABBITMQ_PASS') }}

The rendered configuration file would use environment variables or other secret management patterns to populate these fields.

Container Entrypoint Script

Another method to manage secrets effectively can involve using an entrypoint script to pull secrets from a secure location at container startup, setting them as environment variables. Below is a simple shell script that simulates this:

#!/bin/sh
# entrypoint.sh

RABBITMQ_USER=$(cat /run/secrets/rabbitmq_user)
export RABBITMQ_DEFAULT_USER=$RABBITMQ_USER

RABBITMQ_PASS=$(cat /run/secrets/rabbitmq_pass)
export RABBITMQ_DEFAULT_PASS=$RABBITMQ_PASS

exec "$@"

Then, in your Dockerfile, you must copy this script into the container and set it as the entrypoint:

FROM mcr.microsoft.com/dotnet/aspnet:${SDK_VERSION} AS runtime
WORKDIR /app
COPY --from=build /out ./
COPY entrypoint.sh /entrypoint.sh
RUN chmod +x /entrypoint.sh
ENTRYPOINT ["/entrypoint.sh"]

Conclusion

By employing the strategies outlined—using environment variables, Docker secrets, Jinja templates for dynamic configurations, and entrypoint scripts—OpenTelemetry .NET applications can manage sensitive information effectively in production environments. It is crucial to keep sensitive data out of code repositories and ensure that access to secrets is secured and restricted to essential applications only.

Sources:

  • docker-compose.yml
  • Dockerfile