We use cookies to enhance your experience on the site
CodeWorlds

Secrets Management - the Empire's sealed dispatches

You met the

.env
file in the introductory module and locally it works perfectly. In production it is different - and it is worth knowing why, before somebody convinces you that adding it to
.gitignore
is enough.

Four reasons at once. The risk of an accidental commit - one

git add -f
, or a new developer without a configured
.gitignore
, and the password is in the repository's history forever. No rotation - changing the database password means editing the file by hand on every server. No audit - nobody knows who read a secret, or when. And plain text - passwords sit in the clear on disk, readable by any process with permission.

Note what is not among those reasons:

.env
files are not slow, do not consume memory and work in Node.js perfectly well. The problem is not technical but organisational.

Rome did not send sealed dispatches by an ordinary messenger. They went to a chancery, which issued them against a receipt and knew who had collected what.

The order of .env files

Before we get to the chancery, one thing about the files themselves.

ConfigModule
accepts a list of them:

1ConfigModule.forRoot({
2  isGlobal: true,
3  envFilePath: ['.env.local', '.env.development', '.env'],
4});

The registration order is fixed:

ConfigModule
,
.forRoot({
, the options with
isGlobal: true,
, and finally
})
.

And now the thing that misleads most often: the file listed first wins. When the same variable appears in several files, the value comes from

.env.local
- not from
.env
, despite it looking like the "main" one.

The logic runs from most specific to most general.

.env.local
holds your personal overrides that nobody else has;
.env.development
holds the settings of a whole environment;
.env
holds the defaults. The more specific shadows the more general.

The chancery - HashiCorp Vault

HashiCorp Vault is a tool for centrally managing secrets with auditing and rotation. It is not a frontend framework, not a testing system and not an image compressor - it is a vault that issues values on request and records every issue.

It solves exactly the gaps we listed: the secret does not sit in a file (nothing to commit), it can be changed in one place (rotation), and every read is noted (audit).

The application fetches secrets at startup, before anything needs them:

1export async function loadSecrets(): Promise<Record<string, string>> {
2  const vault = new VaultClient({
3    endpoint: process.env.VAULT_ADDR,
4    token: process.env.VAULT_TOKEN,
5  });
6
7  const { data } = await vault.read('secret/data/legion-api');
8
9  return {
10    DATABASE: data.data.DATABASE,
11    JWT_SECRET: data.data.JWT_SECRET,
12    REDIS_PASSWORD: data.data.REDIS_PASSWORD,
13  };
14}

Note the paradox visible here: to reach the vault you also need a key - an address and a token, which still arrive from environment variables. Vault does not remove that problem, it reduces it to one secret instead of twenty. That one is often additionally issued short-lived by the infrastructure itself.

Priority of sources

In a mature setup secrets may come from several places at once. From highest priority down:

  1. HashiCorp Vault - production; it has auditing and rotation.
  2. AWS Secrets Manager - cloud; the same, but built into the provider's platform.
  3. Environment variables - the fallback; they always work, locally and in tests too.

The rule is simple: the higher, the more control. The environment-variable fallback sits at the bottom not because it is bad but because it offers neither audit nor rotation - while working without any infrastructure at all, so a local run needs no Vault.

Reading in the application

Whatever the source, the code reaches for a secret in the same way:

1const dbUrl = this.configService.get<string>('DATABASE');

The order of the parts is fixed:

this
,
.configService
,
.get<string>
,
('DATABASE')
. It is the same method you met with configuration - and that is the point. The service does not know whether the value came from Vault, from the cloud or from an environment variable; changing the source touches not one line of logic.

Secrets in Kubernetes

A cluster brings an extra difficulty: we describe configuration in YAML files kept in a repository - and a secret in such a file is a password in Git again.

Sealed Secrets allow encrypted secrets to be stored safely in a Git repository. You encrypt a value with the public key of a controller running in the cluster; the resulting file can be committed without worry, because only that controller can decrypt it, with a private key that never leaves the cluster.

This resolves the contradiction between "everything in the repository" and "no secrets in the repository". Sealed Secrets do not scale pods, do not compress images and do not monitor usage - they encrypt secrets so they can travel with the code.

Summary

The chancery issues dispatches against a receipt:

  • .env
    in production is dangerous for four reasons at once: risk of an accidental commit, no rotation, no audit, plain text - not because of speed or memory,
  • in
    envFilePath
    the file listed first wins:
    .env.local
    shadows
    .env.development
    , which shadows
    .env
    ,
  • registration in order:
    ConfigModule
    ,
    .forRoot({
    ,
    isGlobal: true,
    ,
    })
    ,
  • HashiCorp Vault is a tool for centrally managing secrets with auditing and rotation,
  • reaching Vault needs a secret too - but one instead of twenty,
  • priority of sources from the top: Vault (production) → AWS Secrets Manager (cloud) → environment variables (fallback),
  • reading is always the same:
    this
    +
    .configService
    +
    .get<string>
    +
    ('DATABASE')
    - the logic does not know where the value came from,
  • Sealed Secrets allow encrypted secrets to be stored safely in a Git repository - only the controller in the cluster can decrypt them.

In the next lesson we return to the pipeline - this time with build matrices, artifacts and caching. For now remember: a

.env
file tells you what the password is; a chancery also tells you who collected it and when it was changed.

Go to CodeWorlds