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

Use Docker Secrets in dev and production #254

Merged
merged 1 commit into from
Feb 22, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .dockerignore
Original file line number Diff line number Diff line change
Expand Up @@ -12,3 +12,4 @@
/.cache
/public/build
/build
/dev-secrets
8 changes: 4 additions & 4 deletions .env.example
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
# Connect to MySQL Docker container locally
DATABASE_URL="mysql://starchart:starchart_password@localhost:3306/starchart"
# NOTE: this value is also mirrored in dev-secrets/DATABASE_URL.
# We set it here for Prisma scripts to work, which expect it
# in the env, but at runtime we read it as a secret in docker swarm.
DATABASE_URL="mysql://starchart:[email protected]:3306/starchart"

# Connect to Redis container locally
REDIS_URL=redis://localhost:6379

# Used to compute hash against the session IDs. Should be long, random.
SESSION_SECRET="super-duper-s3cret"

# https://letsencrypt.org/docs/expiration-emails/
LETS_ENCRYPT_ACCOUNT_EMAIL="[email protected]"

Expand Down
1 change: 1 addition & 0 deletions .eslintignore
Original file line number Diff line number Diff line change
Expand Up @@ -4,3 +4,4 @@
/mysql-data
/redis-data
/img
/dev-secrets
1 change: 1 addition & 0 deletions .prettierignore
Original file line number Diff line number Diff line change
Expand Up @@ -7,4 +7,5 @@ node_modules
/mysql-data
/img
/playwright-report/
/dev-secrets

23 changes: 23 additions & 0 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@
- [Development](#development)
- [System prerequisites](#system-prerequisites)
- [Dev environment set up](#dev-environment-set-up)
- [`.env` and `./dev-secrets/*`](#env-and-dev-secrets)
- [Usage](#usage)
- [Pull requests](#pull-requests)
- [Merging to main](#merging-to-main)
- [Stages: Draft and Ready for review](#stages-draft-and-ready-for-review)
Expand Down Expand Up @@ -56,6 +58,27 @@ $ npm run db:studio

> **Note** `npm run build` needs to be executed the first time running the project. As it generates a `build/server.js` script that `npm run dev` depends on. Subsequent times, only `npm run dev` is needed to run the app in development mode.

## `.env` and `./dev-secrets/*`

Some application configuration is managed via environment variables, others as secrets (i.e., files).

Your `.env` file should define any environment variables you want to use via `process.env.*`. For example, the line `MY_ENV_VAR=data` in `.env` will mean that `process.env.MY_ENV_VAR` is available at runtime with `data` as its value.

For secrets, we use the [docker-secret](https:/hwkd/docker-secret) module to load and expose secrets via Docker Swarm's [secrets](https://docs.docker.com/engine/swarm/secrets/), which are files that get mounted into the container at `/run/secrets/*`.

In development, if you want to override the Docker secrets used by the app,
set the following in your env: `SECRETS_OVERRIDE=1` (we do this automatically
in many development/testing scripts in `package.json`). This will
load secrets from `./dev-secrets/*` instead of using Docker Swarm secrets. The `./dev-secrets/*` folder contains files we want to expose as secrets to the running app.

### Usage

If you need to add a secret, for example, a secret named `MY_SECRET` with a value of `this-is-secret`:

1. Create a new file `dev-secrets/MY_SECRET` with contents `this-is-secret`
2. In your code, `import secrets from '~/lib/secrets.server'`
3. Use your secret, `secrets.MY_SECRET`

## Pull requests

- To avoid duplicate work, create a draft pull request.
Expand Down
34 changes: 28 additions & 6 deletions app/lib/dns.server.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
import logger from './logger.server';
import {
Route53Client,
CreateHostedZoneCommand,
Expand All @@ -8,6 +7,9 @@ import {
import isFQDN from 'validator/lib/isFQDN';
import isIP from 'validator/lib/isIP';

import logger from '~/lib/logger.server';
import secrets from '~/lib/secrets.server';

import type {
CreateHostedZoneResponse,
ChangeResourceRecordSetsResponse,
Expand All @@ -21,14 +23,34 @@ if (process.env.NODE_ENV === 'production') {
}
}

const { AWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEY } = secrets;

const credentials = () => {
if (process.env.NODE_ENV === 'production') {
if (!AWS_ACCESS_KEY_ID) {
throw new Error('Missing AWS_ACCESS_KEY_ID secret');
}
if (!AWS_SECRET_ACCESS_KEY) {
throw new Error('Missing AWS_SECRET_ACCESS_KEY secret');
}

return {
accessKeyId: AWS_ACCESS_KEY_ID,
secretAccessKey: AWS_SECRET_ACCESS_KEY,
};
}

return {
accessKeyId: process.env.AWS_ACCESS_KEY_ID || AWS_ACCESS_KEY_ID,
secretAccessKey: process.env.AWS_SECRET_ACCESS_KEY || AWS_SECRET_ACCESS_KEY,
sessionToken: process.env.AWS_SESSION_TOKEN,
humphd marked this conversation as resolved.
Show resolved Hide resolved
};
};

export const route53Client = new Route53Client({
endpoint: process.env.AWS_ENDPOINT_URL || 'http://localhost:5053',
region: process.env.AWS_REGION || 'us-east-1',
credentials: {
accessKeyId: process.env.AWS_ACCESS_KEY_ID || '',
secretAccessKey: process.env.AWS_SECRET_ACCESS_KEY || '',
sessionToken: process.env.AWS_SESSION_TOKEN,
},
credentials: credentials(),
});

export const createHostedZone = async (domain: string) => {
Expand Down
24 changes: 12 additions & 12 deletions app/lib/lets-encrypt.server.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,10 @@
import https from 'https';
import dnsPromises from 'dns/promises';
import type { Resolver as PromisesResolver } from 'dns/promises';
import acme from 'acme-client';

import secrets from '~/lib/secrets.server';

import type { Resolver as PromisesResolver } from 'dns/promises';
import type {
Client as AcmeClient,
Order as AcmeOrder,
Expand All @@ -11,7 +14,8 @@ import type {
Account as AcmeAccount,
DnsChallenge as AcmeDnsChallenge,
} from 'acme-client/types/rfc8555';
import { secrets } from 'docker-secret';

const { LETS_ENCRYPT_ACCOUNT_PRIVATE_KEY_PEM } = secrets;

/**
* Get the zone domain (i.e., drop subdomains that are not in the root level of the zone)
Expand Down Expand Up @@ -74,8 +78,6 @@ interface ChallengeBundle extends AcmeDnsChallenge {
value: String;
}

const { LETS_ENCRYPT_ACCOUNT_PRIVATE_KEY_PEM } = secrets ?? {};

/**
* This code is based on the official example
* https:/publishlab/node-acme-client/blob/master/examples/api.js
Expand Down Expand Up @@ -173,21 +175,19 @@ class LetsEncrypt {
};

initialize = async () => {
if (process.env.NODE_ENV === 'production') {
if (!LETS_ENCRYPT_ACCOUNT_PRIVATE_KEY_PEM) {
throw new Error('The docker secret LETS_ENCRYPT_ACCOUNT_PRIVATE_KEY_PEM is missing');
}
if (!LETS_ENCRYPT_ACCOUNT_PRIVATE_KEY_PEM) {
throw new Error('The docker secret LETS_ENCRYPT_ACCOUNT_PRIVATE_KEY_PEM is missing');
}

this.#accountKey = LETS_ENCRYPT_ACCOUNT_PRIVATE_KEY_PEM;

if (process.env.NODE_ENV === 'production') {
if (!process.env.LETS_ENCRYPT_ACCOUNT_EMAIL) {
throw new Error('The env LETS_ENCRYPT_ACCOUNT_EMAIL is missing');
}

this.#accountKey = LETS_ENCRYPT_ACCOUNT_PRIVATE_KEY_PEM;
this.#directoryUrl = acme.directory.letsencrypt.production;
} else {
// For testing and local development, let's use an ad-hoc generated key

this.#accountKey = (await acme.crypto.createPrivateKey()).toString();
this.#directoryUrl = 'https://127.0.0.1:14000/dir';

/**
Expand Down
5 changes: 3 additions & 2 deletions app/lib/notifications.server.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
import { createTransport } from 'nodemailer';
import { secrets } from 'docker-secret';

import secrets from '~/lib/secrets.server';
import logger from './logger.server';

const { NOTIFICATIONS_EMAIL_USER, NODE_ENV, MAILHOG_SMTP_PORT } = process.env;
const { NOTIFICATIONS_USERNAME, NOTIFICATIONS_PASSWORD } = secrets ?? {};
const { NOTIFICATIONS_USERNAME, NOTIFICATIONS_PASSWORD } = secrets;

const initializeTransport = () => {
if (!NOTIFICATIONS_EMAIL_USER) {
Expand Down
9 changes: 9 additions & 0 deletions app/lib/secrets.server.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import { resolve } from 'path';
import { getSecrets } from 'docker-secret';

// To override the secrets we use, set SECRETS_OVERRIDE=1 in the env
const { SECRETS_OVERRIDE } = process.env;
const secretsDir = SECRETS_OVERRIDE && resolve(process.cwd(), './dev-secrets');
const secrets = getSecrets(secretsDir);

export default secrets;
7 changes: 4 additions & 3 deletions app/session.server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,10 @@ import { createCookieSessionStorage, redirect } from '@remix-run/node';

import type { User } from '~/models/user.server';
import { getUserByUsername } from '~/models/user.server';
import secrets from '~/lib/secrets.server';

if (typeof process.env.SESSION_SECRET !== 'string') {
throw new Error('SESSION_SECRET env var must be set');
if (!secrets.SESSION_SECRET?.length) {
throw new Error('SESSION_SECRET must be set');
}

export const sessionStorage = createCookieSessionStorage({
Expand All @@ -13,7 +14,7 @@ export const sessionStorage = createCookieSessionStorage({
httpOnly: true,
path: '/',
sameSite: 'lax',
secrets: [process.env.SESSION_SECRET],
secrets: [secrets.SESSION_SECRET],
secure: process.env.NODE_ENV === 'production',
},
});
Expand Down
1 change: 1 addition & 0 deletions dev-secrets/AWS_ACCESS_KEY_ID
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
testing
1 change: 1 addition & 0 deletions dev-secrets/AWS_SECRET_ACCESS_KEY
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
testing
1 change: 1 addition & 0 deletions dev-secrets/DATABASE_URL
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
mysql://starchart:[email protected]:3306/starchart
27 changes: 27 additions & 0 deletions dev-secrets/LETS_ENCRYPT_ACCOUNT_PRIVATE_KEY_PEM
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
-----BEGIN RSA PRIVATE KEY-----
MIIEowIBAAKCAQEAz1Gi3i5f8ywDNTktilBiSj899kl4X/ZjnI+pTtBz7s95YVwF
+ojDiwkAJHtIQX7XYEHKvJG2pFt4/d72mhUvLXr0hnQLy6cfsPsVgTqV2Um9CSV3
bW+vpO68akC3rXb4lTrBawBI9s/XdBdsVdTDcyeHEvu7q+ahk0wYqNpW7i3h5oBH
8nrcQ0LMPHMeQ68dv+Kzqct9UaPlVZHSfxhUewjpKJfXHJ8pna3G74AJC2+IMj1b
qgJDvp4s2FTTnuOF1jtAe0ZQG/O3UCLJlFoWvrEWK6irXANjg/2p2CdGzlNtsGus
HqXsWH93BgTJrddzS0zAxQr4c4jbIfLXZjvKVQIDAQABAoIBAEGxrblX2qG0vaN8
5dhhVnQOjDTh3RoTekcfIthNp57x8ZWPUnmsIsKI7Jmi0yel6NugnXyZc9WrArD3
mQ8ETXnM73U3ipFS+PDc71iO1vMOsa8XRzvPW0oZOG026r016Nloz+d3JKfI/o3T
6klcbT6tNNkoGbUEFQkW4O2ImmlIYKDfir6gpcR3dVjH/tfJvKtXXgMqK4uLESlK
FZCv/dM/48ZBGUfmFLq0wQLCIqGjrNQHUqHl27EJGO5RNd0hsyKHj8YHCALO2H6M
lEhNH/Jw3iC9lmnmQQHGuxu8L/SEA0F61txYbA+Ci9rHikCdO3/D3ri/VZjjHTx+
Fve2waECgYEA79EeRgIhebnvr6Zhimovx2U6E/znPIwkI97fAQaJiDmAiwlc/M7L
JaQ91YpaEMnMnFZa/DpOtkneSjz9/wo+yeIxp1O7wVbpW0Ggs4gVi1ZiHNQkQJrn
t42AxcADeSoYuSpGITYEOFlT6NV6I2CGwFAqMC/9YGwF47CwlCzOWy0CgYEA3U8b
+AaSAuzECz0iurhXoCgK5GfR3UT62/ml2g2yxlknu+sTKiwb04sQwWLwn/vV80vA
kDZpHQVAjrPRwWhVGMyeYmoJbeUX1F2ix7KIXbQPzVnJ+k+9DIWV906GJ2ZfH2hT
a6HYWEVBYesfOsT+mSfKxsPlXaUOscYN0RnfhMkCgYAXJ+H8cIg68LEsDKyuaMJk
RmntNCY/umhi7koqFy+Ab8zxn93Sq0UCRXGTBODdbh7Lmar/X8Hp6AgGswzza1HU
vHp+5Z7jdDjkDtote55Y7eZbUkCN3GczWf5tGbU8JcxtVJ+g5U5TAo0Plk1MzS01
tIfeT0Pv435OFel25TynoQKBgQDDPjm0lUdXoT5LlAIBrQRRXUJOw3EYvvR6AUNa
nl8sMhel5/weZo+eD8AWfI1A91KHtDsMf5Q4cBvGScox0TPSDyNkO5xaAZUGXB1y
BIXd9S9DwYU5egOU+n2Vkwcz11LwSH/gIwbUyTSniGEi3gynXb29obHG7gmKuOoT
obnaUQKBgAnyof4vHHrFSTzzK0d9M7Ouze5qv7+QUcy4Igt2wItFPgT/HYeq77d5
ztk1dDKLhWCPTHbL/Uvo6DKgw7smnqUq1yly8JeY5a2M7V9xjGkU4a8Y16SukBYk
9y12DbwL03QPe3OkDd6xr8gOYEVVQE2LJ6aK/kIi64ElpSY9cGv3
-----END RSA PRIVATE KEY-----
1 change: 1 addition & 0 deletions dev-secrets/NOTIFICATIONS_PASSWORD
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
dev-smtp-password
1 change: 1 addition & 0 deletions dev-secrets/NOTIFICATIONS_USERNAME
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
dev-smtp-username
1 change: 1 addition & 0 deletions dev-secrets/SESSION_SECRET
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
this-is-our-development-session-secret!!!!
8 changes: 4 additions & 4 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
"build": "run-s build:*",
"build:remix": "remix build",
"build:server": "esbuild --platform=node --format=cjs ./server.ts --outdir=build --bundle",
"dev": "run-p dev:*",
"dev": "SECRETS_OVERRIDE=1 run-p dev:*",
"dev:build": "cross-env NODE_ENV=development npm run build:server -- --watch",
"dev:remix": "cross-env NODE_ENV=development remix watch",
"dev:server": "cross-env NODE_ENV=development node --inspect --require ./node_modules/dotenv/config ./build/server.js",
Expand All @@ -21,10 +21,10 @@
"setup": "run-s db:generate db:push db:seed",
"start": "cross-env NODE_ENV=production node ./build/server.js",
"start:e2e": "cross-env NODE_ENV=production node --require dotenv/config ./build/server.js",
"test": "cross-env NODE_OPTIONS=--require=dotenv/config vitest",
"test": "cross-env SECRETS_OVERRIDE=1 NODE_OPTIONS=--require=dotenv/config vitest",
"test:e2e:dev": "cross-env PORT=8080 start-server-and-test dev http://localhost:8080 \"playwright test\"",
"pretest:e2e:run": "npm run build",
"test:e2e:run": "cross-env PORT=8811 start-server-and-test start:e2e http://localhost:8811 \"playwright test\"",
"pretest:e2e:run": "cross-env SECRETS_OVERRIDE=1 run-s build",
"test:e2e:run": "cross-env SECRETS_OVERRIDE=1 PORT=8811 start-server-and-test start:e2e http://localhost:8811 \"playwright test\"",
"typecheck": "tsc",
"validate": "run-p \"test -- --run\" lint typecheck test:e2e:run",
"prepare": "husky install"
Expand Down