Improve Management Plane: Secrets Management #2

Closed
opened 2024-07-16 14:35:05 -07:00 by Jafner · 11 comments
Owner

Context

Currently our secrets are stored in plain text on the server on which they are needed, and git ignored.

We have some systems for keeping secrets out of version control, but secret management is essentially manual.

  1. Establish specifications.
  2. Evaluate solutions.
  3. Document implementation plan.
  4. Implement.
## Context Currently our secrets are stored in plain text on the server on which they are needed, and git ignored. We have some systems for keeping secrets out of version control, but secret management is essentially manual. 1. Establish specifications. 2. Evaluate solutions. 3. Document implementation plan. 4. Implement.
Author
Owner

Establish Specifications

  • Self-hosted without any ping to external servers.
  • Free.
  • Support Keycloak SSO.
  • Support Docker/Docker Compose.
  • Centralized management; prefer declarative config file, web UI is acceptable.
### Establish Specifications - Self-hosted without any ping to external servers. - Free. - Support Keycloak SSO. - Support Docker/Docker Compose. - Centralized management; prefer declarative config file, web UI is acceptable.
Author
Owner
### Evaluate Solutions - Mozilla's [sops](https://github.com/getsops/sops). - [OneMoreSecret](https://github.com/stud0709/OneMoreSecret) - [Infisical](https://github.com/Infisical/infisical) - [Bitwarden Secrets Manager](https://bitwarden.com/products/secrets-manager/) - [Gitea Secrets](https://docs.gitea.com/usage/secrets) - [HashiCorp Vault](https://www.hashicorp.com/products/vault)
Author
Owner

Intermediate step before adding tools to the toolchain: flatten our environment files into the stack compose file and use docker secrets to set values explicitly.

Example for Keycloak:

Intermediate step before adding tools to the toolchain: flatten our environment files into the stack compose file and use docker secrets to set values explicitly. Example for Keycloak:
Author
Owner

Before: homelab/fighter/config/keycloak/

tree

├── docker-compose.yml
├── forwardauth.env
├── forwardauth-privileged.env
├── forwardauth-privileged_secrets.env
├── forwardauth_secrets.env
├── keycloak.env
├── keycloak_secrets.env
├── postgres.env
├── postgres_secrets.env
└── README.md

1 directory, 10 files

cat docker-compose.yml

services:
  keycloak:
    image: quay.io/keycloak/keycloak:latest
    container_name: keycloak_keycloak
    networks:
      keycloak:
        aliases:
          - keycloak
      web:
        aliases:
          - keycloak
    restart: "no"
    depends_on:
      - postgres
    command: start --hostname=keycloak.jafner.net
    env_file:
      - path: ./keycloak.env
        required: true
      - path: ./keycloak_secrets.env
        required: false
    labels:
      traefik.http.routers.keycloak.rule: Host(`keycloak.jafner.net`)
      traefik.http.routers.keycloak.tls.certresolver: lets-encrypt
      traefik.http.routers.keycloak.middlewares: keycloak-redirect
      traefik.http.services.keycloak.loadbalancer.server.port: 8080
      traefik.http.middlewares.keycloak-redirect.redirectregex.regex: ^https:\\/\\/([^\\//]+)\\/?$$"
      traefik.http.middlewares.keycloak-redirect.redirectregex.replacement: https://$$1/admin"

  forwardauth:
    image: mesosphere/traefik-forward-auth:latest
    container_name: keycloak_forwardauth
    networks:
      web:
        aliases:
          - forwardauth
    restart: "no"
    command: "./traefik-forward-auth"
    depends_on:
      - keycloak
    env_file:
      - path: ./forwardauth.env
        required: true
      - path: ./forwardauth_secrets.env
        required: false
    labels:
      - "traefik.enable=false"
      - "traefik.http.routers.forwardauth.rule=Path(`/_oauth`)"
      - "traefik.http.routers.forwardauth.tls.certresolver=lets-encrypt"

  forwardauth-privileged:
    image: mesosphere/traefik-forward-auth:latest
    container_name: keycloak_forwardauth-privileged
    networks:
      web:
        aliases:
          - forwardauth-privileged
    restart: "no"
    command: "./traefik-forward-auth --whitelist=jafner425@gmail.com"
    depends_on:
      - keycloak
    env_file:
      - path: ./forwardauth-privileged.env
        required: true
      - path: ./forwardauth-privileged_secrets.env
        required: false
    labels:
      - "traefik.enable=false"
      - "traefik.http.routers.forwardauth-privileged.rule=Path(`/_oauth`)"
      - "traefik.http.routers.forwardauth-privileged.tls.certresolver=lets-encrypt"

  postgres:
    image: postgres:latest
    container_name: keycloak_postgres
    networks:
      - keycloak
    env_file:
      - path: ./postgres.env
        required: true
      - path: ./postgres_secrets.env
        required: false
      - postgres.env
      - postgres_secrets.env
    volumes:
      - postgres_data:/var/lib/postgresql/data

networks:
  web:
    external: true
  keycloak:

volumes:
  postgres_data:
# Before: `homelab/fighter/config/keycloak/` ### `tree` ``` ├── docker-compose.yml ├── forwardauth.env ├── forwardauth-privileged.env ├── forwardauth-privileged_secrets.env ├── forwardauth_secrets.env ├── keycloak.env ├── keycloak_secrets.env ├── postgres.env ├── postgres_secrets.env └── README.md 1 directory, 10 files ``` ### `cat docker-compose.yml` ```yaml services: keycloak: image: quay.io/keycloak/keycloak:latest container_name: keycloak_keycloak networks: keycloak: aliases: - keycloak web: aliases: - keycloak restart: "no" depends_on: - postgres command: start --hostname=keycloak.jafner.net env_file: - path: ./keycloak.env required: true - path: ./keycloak_secrets.env required: false labels: traefik.http.routers.keycloak.rule: Host(`keycloak.jafner.net`) traefik.http.routers.keycloak.tls.certresolver: lets-encrypt traefik.http.routers.keycloak.middlewares: keycloak-redirect traefik.http.services.keycloak.loadbalancer.server.port: 8080 traefik.http.middlewares.keycloak-redirect.redirectregex.regex: ^https:\\/\\/([^\\//]+)\\/?$$" traefik.http.middlewares.keycloak-redirect.redirectregex.replacement: https://$$1/admin" forwardauth: image: mesosphere/traefik-forward-auth:latest container_name: keycloak_forwardauth networks: web: aliases: - forwardauth restart: "no" command: "./traefik-forward-auth" depends_on: - keycloak env_file: - path: ./forwardauth.env required: true - path: ./forwardauth_secrets.env required: false labels: - "traefik.enable=false" - "traefik.http.routers.forwardauth.rule=Path(`/_oauth`)" - "traefik.http.routers.forwardauth.tls.certresolver=lets-encrypt" forwardauth-privileged: image: mesosphere/traefik-forward-auth:latest container_name: keycloak_forwardauth-privileged networks: web: aliases: - forwardauth-privileged restart: "no" command: "./traefik-forward-auth --whitelist=jafner425@gmail.com" depends_on: - keycloak env_file: - path: ./forwardauth-privileged.env required: true - path: ./forwardauth-privileged_secrets.env required: false labels: - "traefik.enable=false" - "traefik.http.routers.forwardauth-privileged.rule=Path(`/_oauth`)" - "traefik.http.routers.forwardauth-privileged.tls.certresolver=lets-encrypt" postgres: image: postgres:latest container_name: keycloak_postgres networks: - keycloak env_file: - path: ./postgres.env required: true - path: ./postgres_secrets.env required: false - postgres.env - postgres_secrets.env volumes: - postgres_data:/var/lib/postgresql/data networks: web: external: true keycloak: volumes: postgres_data: ```
Author
Owner

After: /homelab/fighter/config/keycloak/

tree

├── docker-compose.yml
├── README.md
└── secrets
    ├── forwardauth_CLIENT_SECRET.txt
    ├── forwardauth_ENCRYPTION_KEY.txt
    ├── forwardauth_privileged_CLIENT_SECRET.txt
    ├── forwardauth_privileged_ENCRYPTION_KEY.txt
    ├── forwardauth_privileged_SECRET.txt
    ├── forwardauth_SECRET.txt
    ├── keycloak_DB_PASS.txt
    ├── keycloak_KC_DB_PASSWORD.txt
    ├── keycloak_KEYCLOAK_ADMIN_PASSWORD.txt
    ├── postgres_POSTGRES_PASSWORD.txt
    └── postgres_POSTGRES_USER.txt

2 directories, 13 files

cat docker-compose.yml


services:
  keycloak:
    image: quay.io/keycloak/keycloak:latest
    container_name: keycloak_keycloak
    networks:
      keycloak:
        aliases:
          - keycloak
      web:
        aliases:
          - keycloak
    restart: "no"
    depends_on:
      - postgres
    command: start --hostname=keycloak.jafner.net
    secrets:
      - keycloak_KC_DB_PASSWORD
      - keycloak_DB_PASS
      - keycloak_KEYCLOAK_ADMIN_PASSWORD
    environment:
      KC_DB: postgres
      KC_DB_URL: jdbc:postgresql://postgres/keycloak
      KC_DB_USERNAME: keycloak
      KC_HOSTNAME_URL: https://keycloak.jafner.net
      KC_PROXY: edge
      KC_HEALTH_ENABLED: true
      KC_METRICS_ENABLED: true
      KEYCLOAK_ADMIN: Jafner
      KC_DB_PASSWORD: /run/secrets/keycloak_KC_DB_PASSWORD
      DB_PASS: /run/secrets/keycloak_DB_PASS
      KEYCLOAK_ADMIN_PASSWORD: /run/secrets/keycloak_KEYCLOAK_ADMIN_PASSWORD
    labels:
      traefik.http.routers.keycloak.rule: Host(`keycloak.jafner.net`)
      traefik.http.routers.keycloak.tls.certresolver: lets-encrypt
      traefik.http.routers.keycloak.middlewares: keycloak-redirect
      traefik.http.services.keycloak.loadbalancer.server.port: 8080
      traefik.http.middlewares.keycloak-redirect.redirectregex.regex: ^https:\\/\\/([^\\//]+)\\/?$$"
      traefik.http.middlewares.keycloak-redirect.redirectregex.replacement: https://$$1/admin"

  forwardauth:
    image: mesosphere/traefik-forward-auth:latest
    container_name: keycloak_forwardauth
    networks:
      web:
        aliases:
          - forwardauth
    restart: "no"
    command: "./traefik-forward-auth"
    depends_on:
      - keycloak
    env_file:
      - path: ./forwardauth.env
        required: true
      - path: ./forwardauth_secrets.env
        required: false
    secrets:
      - forwardauth_CLIENT_SECRET
      - forwardauth_SECRET
      - forwardauth_ENCRYPTION_KEY
    environment:
      PROVIDER_URI: "https://keycloak.jafner.net/realms/Jafner.net"
      CLIENT_ID: "traefik-forward-auth"
      LOG_LEVEL: "debug"
      CLIENT_SECRET: /run/secrets/forwardauth_CLIENT_SECRET
      SECRET: /run/secrets/forwardauth_SECRET
      ENCRYPTION_KEY: /run/secrets/forwardauth_ENCRYPTION_KEY
    labels:
      - "traefik.enable=false"
      - "traefik.http.routers.forwardauth.rule=Path(`/_oauth`)"
      - "traefik.http.routers.forwardauth.tls.certresolver=lets-encrypt"

  forwardauth-privileged:
    image: mesosphere/traefik-forward-auth:latest
    container_name: keycloak_forwardauth-privileged
    networks:
      web:
        aliases:
          - forwardauth-privileged
    restart: "no"
    command: "./traefik-forward-auth --whitelist=jafner425@gmail.com"
    depends_on:
      - keycloak
    secrets:
      - forwardauth_privileged_CLIENT_SECRET
      - forwardauth_privileged_SECRET
      - forwardauth_privileged_ENCRYPTION_KEY
    environment:
      PROVIDER_URI: "https://keycloak.jafner.net/realms/Jafner.net"
      CLIENT_ID: "traefik-forward-auth-privileged"
      LOG_LEVEL: "debug"
      CLIENT_SECRET: /run/secrets/forwardauth_privileged_CLIENT_SECRET
      SECRET: /run/secrets/forwardauth_privileged_SECRET
      ENCRYPTION_KEY: /run/secrets/forwardauth_privileged_ENCRYPTION_KEY
    labels:
      - "traefik.enable=false"
      - "traefik.http.routers.forwardauth-privileged.rule=Path(`/_oauth`)"
      - "traefik.http.routers.forwardauth-privileged.tls.certresolver=lets-encrypt"

  postgres:
    image: postgres:latest
    container_name: keycloak_postgres
    networks:
      - keycloak
    secrets:
      - postgres_POSTGRES_USER
      - postgres_POSTGRES_PASSWORD
    environment:
      POSTGRES_DB: keycloak
      POSTGRES_USER: /run/secrets/postgres_POSTGRES_USER
      POSTGRES_PASSWORD: /run/secrets/postgres_POSTGRES_PASSWORD
    volumes:
      - postgres_data:/var/lib/postgresql/data

networks:
  web:
    external: true
  keycloak:

volumes:
  postgres_data:

secrets:
  forwardauth_privileged_CLIENT_SECRET:
    file: ./secrets/forwardauth_privileged_CLIENT_SECRET.txt
  forwardauth_privileged_SECRET: 
    file: ./secrets/forwardauth_privileged_SECRET.txt
  forwardauth_privileged_ENCRYPTION_KEY: 
    file: ./secrets/forwardauth_privileged_ENCRYPTION_KEY.txt
  forwardauth_CLIENT_SECRET:
    file: ./secrets/forwardauth_CLIENT_SECRET.txt
  forwardauth_SECRET:
    file: ./secrets/forwardauth_SECRET.txt
  forwardauth_ENCRYPTION_KEY:
    file: ./secrets/forwardauth_ENCRYPTION_KEY.txt
  keycloak_KC_DB_PASSWORD: 
    file: ./secrets/keycloak_KC_DB_PASSWORD.txt
  keycloak_DB_PASS: 
    file: ./secrets/keycloak_DB_PASS.txt
  keycloak_KEYCLOAK_ADMIN_PASSWORD: 
    file: ./secrets/keycloak_KEYCLOAK_ADMIN_PASSWORD.txt
  postgres_POSTGRES_USER: 
    file: ./secrets/postgres_POSTGRES_USER.txt
  postgres_POSTGRES_PASSWORD: 
    file: ./secrets/postgres_POSTGRES_PASSWORD.txt
# After: `/homelab/fighter/config/keycloak/` ### `tree` ``` ├── docker-compose.yml ├── README.md └── secrets ├── forwardauth_CLIENT_SECRET.txt ├── forwardauth_ENCRYPTION_KEY.txt ├── forwardauth_privileged_CLIENT_SECRET.txt ├── forwardauth_privileged_ENCRYPTION_KEY.txt ├── forwardauth_privileged_SECRET.txt ├── forwardauth_SECRET.txt ├── keycloak_DB_PASS.txt ├── keycloak_KC_DB_PASSWORD.txt ├── keycloak_KEYCLOAK_ADMIN_PASSWORD.txt ├── postgres_POSTGRES_PASSWORD.txt └── postgres_POSTGRES_USER.txt 2 directories, 13 files ``` ### `cat docker-compose.yml` ```yaml services: keycloak: image: quay.io/keycloak/keycloak:latest container_name: keycloak_keycloak networks: keycloak: aliases: - keycloak web: aliases: - keycloak restart: "no" depends_on: - postgres command: start --hostname=keycloak.jafner.net secrets: - keycloak_KC_DB_PASSWORD - keycloak_DB_PASS - keycloak_KEYCLOAK_ADMIN_PASSWORD environment: KC_DB: postgres KC_DB_URL: jdbc:postgresql://postgres/keycloak KC_DB_USERNAME: keycloak KC_HOSTNAME_URL: https://keycloak.jafner.net KC_PROXY: edge KC_HEALTH_ENABLED: true KC_METRICS_ENABLED: true KEYCLOAK_ADMIN: Jafner KC_DB_PASSWORD: /run/secrets/keycloak_KC_DB_PASSWORD DB_PASS: /run/secrets/keycloak_DB_PASS KEYCLOAK_ADMIN_PASSWORD: /run/secrets/keycloak_KEYCLOAK_ADMIN_PASSWORD labels: traefik.http.routers.keycloak.rule: Host(`keycloak.jafner.net`) traefik.http.routers.keycloak.tls.certresolver: lets-encrypt traefik.http.routers.keycloak.middlewares: keycloak-redirect traefik.http.services.keycloak.loadbalancer.server.port: 8080 traefik.http.middlewares.keycloak-redirect.redirectregex.regex: ^https:\\/\\/([^\\//]+)\\/?$$" traefik.http.middlewares.keycloak-redirect.redirectregex.replacement: https://$$1/admin" forwardauth: image: mesosphere/traefik-forward-auth:latest container_name: keycloak_forwardauth networks: web: aliases: - forwardauth restart: "no" command: "./traefik-forward-auth" depends_on: - keycloak env_file: - path: ./forwardauth.env required: true - path: ./forwardauth_secrets.env required: false secrets: - forwardauth_CLIENT_SECRET - forwardauth_SECRET - forwardauth_ENCRYPTION_KEY environment: PROVIDER_URI: "https://keycloak.jafner.net/realms/Jafner.net" CLIENT_ID: "traefik-forward-auth" LOG_LEVEL: "debug" CLIENT_SECRET: /run/secrets/forwardauth_CLIENT_SECRET SECRET: /run/secrets/forwardauth_SECRET ENCRYPTION_KEY: /run/secrets/forwardauth_ENCRYPTION_KEY labels: - "traefik.enable=false" - "traefik.http.routers.forwardauth.rule=Path(`/_oauth`)" - "traefik.http.routers.forwardauth.tls.certresolver=lets-encrypt" forwardauth-privileged: image: mesosphere/traefik-forward-auth:latest container_name: keycloak_forwardauth-privileged networks: web: aliases: - forwardauth-privileged restart: "no" command: "./traefik-forward-auth --whitelist=jafner425@gmail.com" depends_on: - keycloak secrets: - forwardauth_privileged_CLIENT_SECRET - forwardauth_privileged_SECRET - forwardauth_privileged_ENCRYPTION_KEY environment: PROVIDER_URI: "https://keycloak.jafner.net/realms/Jafner.net" CLIENT_ID: "traefik-forward-auth-privileged" LOG_LEVEL: "debug" CLIENT_SECRET: /run/secrets/forwardauth_privileged_CLIENT_SECRET SECRET: /run/secrets/forwardauth_privileged_SECRET ENCRYPTION_KEY: /run/secrets/forwardauth_privileged_ENCRYPTION_KEY labels: - "traefik.enable=false" - "traefik.http.routers.forwardauth-privileged.rule=Path(`/_oauth`)" - "traefik.http.routers.forwardauth-privileged.tls.certresolver=lets-encrypt" postgres: image: postgres:latest container_name: keycloak_postgres networks: - keycloak secrets: - postgres_POSTGRES_USER - postgres_POSTGRES_PASSWORD environment: POSTGRES_DB: keycloak POSTGRES_USER: /run/secrets/postgres_POSTGRES_USER POSTGRES_PASSWORD: /run/secrets/postgres_POSTGRES_PASSWORD volumes: - postgres_data:/var/lib/postgresql/data networks: web: external: true keycloak: volumes: postgres_data: secrets: forwardauth_privileged_CLIENT_SECRET: file: ./secrets/forwardauth_privileged_CLIENT_SECRET.txt forwardauth_privileged_SECRET: file: ./secrets/forwardauth_privileged_SECRET.txt forwardauth_privileged_ENCRYPTION_KEY: file: ./secrets/forwardauth_privileged_ENCRYPTION_KEY.txt forwardauth_CLIENT_SECRET: file: ./secrets/forwardauth_CLIENT_SECRET.txt forwardauth_SECRET: file: ./secrets/forwardauth_SECRET.txt forwardauth_ENCRYPTION_KEY: file: ./secrets/forwardauth_ENCRYPTION_KEY.txt keycloak_KC_DB_PASSWORD: file: ./secrets/keycloak_KC_DB_PASSWORD.txt keycloak_DB_PASS: file: ./secrets/keycloak_DB_PASS.txt keycloak_KEYCLOAK_ADMIN_PASSWORD: file: ./secrets/keycloak_KEYCLOAK_ADMIN_PASSWORD.txt postgres_POSTGRES_USER: file: ./secrets/postgres_POSTGRES_USER.txt postgres_POSTGRES_PASSWORD: file: ./secrets/postgres_POSTGRES_PASSWORD.txt ```
Author
Owner

The latter consolidates all configuration details (except the actual secret values) into the compose.

It would be a bit inconvenient to manually create each of these text files one at a time.

So instead we can keep a stack-level secrets.env file (or similar name) populated with the file names and values of each secret, then run a one-liner to create each secret file during deployment.

One-liner: mkdir -p ./secrets; for secret in $(cat secrets.env); do name=${secret%%=*}; value=${secret#*=}; echo $value > ./secrets/$name.txt; done

This strategy integrates nicely with CI/CD if we want to use (for example) SOPS + age to store the secrets.env file with its values (but not keys) encrypted in the repository. At the same time, we can manage the secrets manually without much hassle so long as we keep the secrets.env file out of source control.

The latter consolidates all configuration details (except the actual secret values) into the compose. It would be a bit inconvenient to manually create each of these text files one at a time. So instead we can keep a stack-level `secrets.env` file (or similar name) populated with the file names and values of each secret, then run a one-liner to create each secret file during deployment. One-liner: `mkdir -p ./secrets; for secret in $(cat secrets.env); do name=${secret%%=*}; value=${secret#*=}; echo $value > ./secrets/$name.txt; done` This strategy integrates nicely with CI/CD if we want to use (for example) SOPS + age to store the `secrets.env` file with its values (but not keys) encrypted in the repository. At the same time, we can manage the secrets manually without much hassle so long as we keep the `secrets.env` file out of source control.
Author
Owner

We can accelerate the migration with a script to parse our existing _secrets.env files and consolidate them into a new secrets.env stack-level file.

for file in *_secrets.env; do 
    service="${file%%_*}_"
    for secret in $(cat $file); do
        key=${secret%%=*}
        value=${secret#*=}
        value=${value%\'}
        value=${value#\'}
        echo "$service$key=$value" >> secrets.env
    done 
done

Although this doesn't help us refactor our compose files...

We can accelerate the migration with a script to parse our existing `_secrets.env` files and consolidate them into a new `secrets.env` stack-level file. ```bash for file in *_secrets.env; do service="${file%%_*}_" for secret in $(cat $file); do key=${secret%%=*} value=${secret#*=} value=${value%\'} value=${value#\'} echo "$service$key=$value" >> secrets.env done done ``` Although this doesn't help us refactor our compose files...
Jafner referenced this issue from a commit 2024-08-14 14:30:13 -07:00
Author
Owner

After getting some experience with sops and age, as well as git-crypt, I think that adding a secrets management service (such as Vault or Delinea) would offer little value while we've got only a single developer on the team.

If Docker/Compose secrets had tighter integration and our environment had more moving parts, it may make sense to centralize our secrets and take advantage of easier rotation and sharing, but presently all (?) of our secrets are simple configuration parameters passed into Stacks at launch, and are extremely simple to manage.

After getting some experience with sops and age, as well as git-crypt, I think that adding a secrets management service (such as Vault or Delinea) would offer little value while we've got only a single developer on the team. If Docker/Compose secrets had tighter integration **and** our environment had more moving parts, it may make sense to centralize our secrets and take advantage of easier rotation and sharing, but presently all (?) of our secrets are simple configuration parameters passed into Stacks at launch, and are extremely simple to manage.
Author
Owner

With that in mind, and with some further reflection on the discussions in #3 and #7, here's the management plane I think we've settled on for this chapter:

  • Most secrets are defined in secrets.env as simple key-value pairs that we want to use in our Compose stack.
  • We use sops to encrypt the values in our secrets file with age.
  • All secrets at rest are encrypted with a developer private key, and against the CI/CD pubkey and the pubkey(s) of the host(s) to which the secret will be deployed.
  • Modifying secrets from a developer machine should use the following workflow components:
    • a Git filter to automatically create a decrypted~$file_name file at the location of the secret.
    • script to idempotently re-encrypt all secrets against their intended targets when either the secret or target list is changed.

Managing Decryption Keys

Our secrets are accessible through the Docker host onto which they are deployed (via docker inspecting env vars). So efforts to obfuscate the values on the host are mostly pointless. However, it would be preferred to avoid keeping a decryption key on the host.

The CI/CD environment has a key, and each deploy target (host) has a key. I believe sops supports configuring a secret to require more than one key to fully decrypt the secret, which would mean that we could disallow the host or runner from decrypting the secret alone.

For this, we would need to ensure the deploy host has a private key located at a well-known path, and a pubkey listed in the repository. We would also need to configure our Actions environment with an age keypair.

Edit: see sops key groups for reference on configuring multi-key decryption.

With that in mind, and with some further reflection on the discussions in #3 and #7, here's the management plane I think we've settled on for this chapter: - Most secrets are defined in `secrets.env` as simple key-value pairs that we want to use in our Compose stack. - We use sops to encrypt the values in our secrets file with age. - All secrets at rest are encrypted with a developer private key, and against the CI/CD pubkey *and* the pubkey(s) of the host(s) to which the secret will be deployed. - Modifying secrets from a developer machine should use the following workflow components: - a Git filter to automatically create a `decrypted~$file_name` file at the location of the secret. - script to idempotently re-encrypt all secrets against their intended targets when either the secret or target list is changed. ### Managing Decryption Keys Our secrets are accessible through the Docker host onto which they are deployed (via `docker inspect`ing env vars). So efforts to obfuscate the values on the host are mostly pointless. However, it would be preferred to avoid keeping a decryption key on the host. The CI/CD environment has a key, and each deploy target (host) has a key. I believe sops supports configuring a secret to require more than one key to fully decrypt the secret, which would mean that we could disallow the host *or* runner from decrypting the secret alone. For this, we would need to ensure the deploy host has a private key located at a well-known path, and a pubkey listed in the repository. We would also need to configure our Actions environment with an age keypair. Edit: see [sops key groups](https://github.com/getsops/sops?tab=readme-ov-file#215key-groups) for reference on configuring multi-key decryption.
Author
Owner

Secrets Modification Workflow Considerations

  • Secrets should always be encrypted at rest in git.
  • Secrets should only be decrypted on the development machine when they are being edited. (No git smudge filter to decrypt on checkout).
  • Committing modified secrets should not require extra steps from the developer (no running a command or script manually to encrypt before committing).
  • Using sops and age, encrypting the same content against the same recipients with different private keys results in different content. This can create challenges with any workflow that attempts to make both encryption and decryption transparent to the developer via git filters.

Consider the following:

  1. Developer A creates a secrets file. They save and commit the file, relying on a git clean filter to ensure the secret is encrypted in version control.
  2. Developer A's git filter uses their private key to encrypt the secret against a list of two pubkeys: DevA, DevB.
  3. Developer B checks out the repo and relies on a git smudge filter to automatically decrypt the secret file into plaintext.
  4. Without issue, Developer B can read the encrypted file as plain text. However, on Developer B's machine, git will find differences between the (encrypted) versions of the local and origin copies of the secret, despite their decrypted contents not changing. We may be able to work around this like so
### Secrets Modification Workflow Considerations - Secrets should always be encrypted at rest in git. - Secrets should only be decrypted on the development machine when they are being edited. (No git smudge filter to decrypt on checkout). - Committing modified secrets should not require extra steps from the developer (no running a command or script manually to encrypt before committing). - Using sops and age, encrypting the same content against the same recipients with different private keys results in different content. This can create challenges with any workflow that attempts to make both encryption and decryption transparent to the developer via git filters. Consider the following: 1. Developer A creates a secrets file. They save and commit the file, relying on a git clean filter to ensure the secret is encrypted in version control. 2. Developer A's git filter uses their private key to encrypt the secret against a list of two pubkeys: DevA, DevB. 3. Developer B checks out the repo and relies on a git smudge filter to automatically decrypt the secret file into plaintext. 4. Without issue, Developer B can read the encrypted file as plain text. However, on Developer B's machine, git will find differences between the (encrypted) versions of the local and origin copies of the secret, despite their decrypted contents not changing. We may be able to work around this [like so](https://github.com/getsops/sops?tab=readme-ov-file#48showing-diffs-in-cleartext-in-git)
Author
Owner

One of the guiding principles for this repo is declarative-ness. As such, using sops to store our secrets securely and declaratively is probably the simplest valid management plane for us.

We're closing this issue until further change is necessary.

One of the guiding principles for this repo is declarative-ness. As such, using sops to store our secrets securely and declaratively is probably the simplest valid management plane for us. We're closing this issue until further change is necessary.
Sign in to join this conversation.
No Label
No Milestone
No project
No Assignees
1 Participants
Notifications
Due Date
The due date is invalid or out of range. Please use the format 'yyyy-mm-dd'.

No due date set.

Dependencies

No dependencies set.

Reference: Jafner/Jafner.net#2
No description provided.