From 56b57ad171da170c2d8edd6112092cef0b586852 Mon Sep 17 00:00:00 2001 From: Meier Lukas Date: Tue, 31 Dec 2024 11:30:29 +0100 Subject: [PATCH] fix(docker): replace anonymous docker volume with env variable for encrypting secrets (#1809) --- .env.example | 13 ++++++++----- Dockerfile | 8 ++------ apps/nextjs/next.config.mjs | 1 + e2e/shared/create-homarr-container.ts | 21 +++++++++++++++++--- packages/auth/env.mjs | 2 -- packages/common/env.mjs | 28 +++++++++++++++++++++++++++ packages/common/package.json | 3 ++- packages/common/src/encryption.ts | 16 ++++----------- scripts/generateRandomSecureKey.js | 7 ------- scripts/run.sh | 27 ++------------------------ turbo.json | 2 +- 11 files changed, 66 insertions(+), 62 deletions(-) create mode 100644 packages/common/env.mjs delete mode 100644 scripts/generateRandomSecureKey.js diff --git a/.env.example b/.env.example index b05d685b1..854c2b19a 100644 --- a/.env.example +++ b/.env.example @@ -4,6 +4,14 @@ # This file will be committed to version control, so make sure not to have any secrets in it. # If you are cloning this repo, create a copy of this file named `.env` and populate it with your secrets. +# The below secret is not used anywhere but required for Auth.js (Would encrypt JWTs and Mail hashes, both not used) +AUTH_SECRET="supersecret" + +# The below secret is used to encrypt integration secrets in the database. +# It should be a 32-byte string, generated by running `openssl rand -hex 32` on Unix +# or starting the project without any (which will show a randomly generated one). +SECRET_ENCRYPTION_KEY=0000000000000000000000000000000000000000000000000000000000000000 + # This is how you can use the sqlite driver: DB_DRIVER='better-sqlite3' DB_URL='FULL_PATH_TO_YOUR_SQLITE_DB_FILE' @@ -20,11 +28,6 @@ DB_URL='FULL_PATH_TO_YOUR_SQLITE_DB_FILE' # DB_PASSWORD='password' # DB_NAME='name-of-database' - -# You can generate the secret via 'openssl rand -base64 32' on Unix -# @see https://next-auth.js.org/configuration/options#secret -AUTH_SECRET='supersecret' - TURBO_TELEMETRY_DISABLED=1 # Configure logging to use winston logger diff --git a/Dockerfile b/Dockerfile index 3e6f04281..65e174659 100644 --- a/Dockerfile +++ b/Dockerfile @@ -25,12 +25,10 @@ RUN corepack enable pnpm && pnpm build FROM base AS runner WORKDIR /app -# gettext is required for envsubst -RUN apk add --no-cache redis nginx bash gettext su-exec +# gettext is required for envsubst, openssl for generating AUTH_SECRET, su-exec for running application as non-root +RUN apk add --no-cache redis nginx bash gettext su-exec openssl RUN mkdir /appdata VOLUME /appdata -RUN mkdir /secrets -VOLUME /secrets @@ -43,7 +41,6 @@ RUN echo $'#!/bin/bash\ncd /app/apps/cli && node ./cli.cjs "$@"' > /usr/bin/homa RUN chmod +x /usr/bin/homarr # Don't run production as root -RUN chown -R nextjs:nodejs /secrets RUN mkdir -p /var/cache/nginx && chown -R nextjs:nodejs /var/cache/nginx && \ mkdir -p /var/log/nginx && chown -R nextjs:nodejs /var/log/nginx && \ mkdir -p /var/lib/nginx && chown -R nextjs:nodejs /var/lib/nginx && \ @@ -67,7 +64,6 @@ COPY --from=builder --chown=nextjs:nodejs /app/apps/nextjs/public ./apps/nextjs/ COPY --chown=nextjs:nodejs scripts/run.sh ./run.sh COPY scripts/entrypoint.sh ./entrypoint.sh RUN chmod +x ./entrypoint.sh -COPY --chown=nextjs:nodejs scripts/generateRandomSecureKey.js ./generateRandomSecureKey.js COPY --chown=nextjs:nodejs packages/redis/redis.conf /app/redis.conf COPY --chown=nextjs:nodejs nginx.conf /etc/nginx/templates/nginx.conf diff --git a/apps/nextjs/next.config.mjs b/apps/nextjs/next.config.mjs index 8c4bd2800..bd57eb1a2 100644 --- a/apps/nextjs/next.config.mjs +++ b/apps/nextjs/next.config.mjs @@ -1,6 +1,7 @@ // Importing env files here to validate on build import "@homarr/auth/env.mjs"; import "@homarr/db/env.mjs"; +import "@homarr/common/env.mjs"; import MillionLint from "@million/lint"; import createNextIntlPlugin from "next-intl/plugin"; diff --git a/e2e/shared/create-homarr-container.ts b/e2e/shared/create-homarr-container.ts index e6f2281ae..3183e66ca 100644 --- a/e2e/shared/create-homarr-container.ts +++ b/e2e/shared/create-homarr-container.ts @@ -5,7 +5,22 @@ export const createHomarrContainer = () => { throw new Error("This test should only be run in CI or with a homarr image named 'homarr-e2e'"); } - return new GenericContainer("homarr-e2e") - .withExposedPorts(7575) - .withWaitStrategy(Wait.forHttp("/api/health/ready", 7575)); + return withLogs( + new GenericContainer("homarr-e2e") + .withExposedPorts(7575) + .withEnvironment({ + SECRET_ENCRYPTION_KEY: "0".repeat(64), + }) + .withWaitStrategy(Wait.forHttp("/api/health/ready", 7575)), + ); +}; + +export const withLogs = (container: GenericContainer) => { + container.withLogConsumer((stream) => + stream + .on("data", (line) => console.log(line)) + .on("err", (line) => console.error(line)) + .on("end", () => console.log("Stream closed")), + ); + return container; }; diff --git a/packages/auth/env.mjs b/packages/auth/env.mjs index de1faa36f..ebc4555df 100644 --- a/packages/auth/env.mjs +++ b/packages/auth/env.mjs @@ -64,7 +64,6 @@ export const env = createEnv({ server: { AUTH_LOGOUT_REDIRECT_URL: z.string().url().optional(), AUTH_SESSION_EXPIRY_TIME: createDurationSchema("30d"), - AUTH_SECRET: process.env.NODE_ENV === "production" ? z.string().min(1) : z.string().min(1).optional(), AUTH_PROVIDERS: authProvidersSchema, ...(authProviders.includes("oidc") ? { @@ -98,7 +97,6 @@ export const env = createEnv({ runtimeEnv: { AUTH_LOGOUT_REDIRECT_URL: process.env.AUTH_LOGOUT_REDIRECT_URL, AUTH_SESSION_EXPIRY_TIME: process.env.AUTH_SESSION_EXPIRY_TIME, - AUTH_SECRET: process.env.AUTH_SECRET, AUTH_PROVIDERS: process.env.AUTH_PROVIDERS, AUTH_LDAP_BASE: process.env.AUTH_LDAP_BASE, AUTH_LDAP_BIND_DN: process.env.AUTH_LDAP_BIND_DN, diff --git a/packages/common/env.mjs b/packages/common/env.mjs new file mode 100644 index 000000000..4e62ec25d --- /dev/null +++ b/packages/common/env.mjs @@ -0,0 +1,28 @@ +import { randomBytes } from "crypto"; +import { createEnv } from "@t3-oss/env-nextjs"; +import { z } from "zod"; + +const errorSuffix = `, please generate a 64 character secret in hex format or use the following: "${randomBytes(32).toString("hex")}"`; + +export const env = createEnv({ + server: { + SECRET_ENCRYPTION_KEY: z + .string({ + required_error: `SECRET_ENCRYPTION_KEY is required${errorSuffix}`, + }) + .min(64, { + message: `SECRET_ENCRYPTION_KEY has to be 64 characters${errorSuffix}`, + }) + .max(64, { + message: `SECRET_ENCRYPTION_KEY has to be 64 characters${errorSuffix}`, + }) + .regex(/^[0-9a-fA-F]{64}$/, { + message: `SECRET_ENCRYPTION_KEY must only contain hex characters${errorSuffix}`, + }), + }, + runtimeEnv: { + SECRET_ENCRYPTION_KEY: process.env.SECRET_ENCRYPTION_KEY, + }, + skipValidation: + Boolean(process.env.CI) || Boolean(process.env.SKIP_ENV_VALIDATION) || process.env.npm_lifecycle_event === "lint", +}); diff --git a/packages/common/package.json b/packages/common/package.json index 5b54f768c..b294e9abf 100644 --- a/packages/common/package.json +++ b/packages/common/package.json @@ -8,7 +8,8 @@ ".": "./index.ts", "./types": "./src/types.ts", "./server": "./src/server.ts", - "./client": "./src/client.ts" + "./client": "./src/client.ts", + "./env.mjs": "./env.mjs" }, "typesVersions": { "*": { diff --git a/packages/common/src/encryption.ts b/packages/common/src/encryption.ts index 369fac3b2..e9a20b69c 100644 --- a/packages/common/src/encryption.ts +++ b/packages/common/src/encryption.ts @@ -1,20 +1,12 @@ import crypto from "crypto"; -import { logger } from "@homarr/log"; +import { env } from "../env.mjs"; const algorithm = "aes-256-cbc"; //Using AES encryption -const fallbackKey = "0000000000000000000000000000000000000000000000000000000000000000"; -const encryptionKey = process.env.ENCRYPTION_KEY ?? fallbackKey; // Fallback to a default key for local development -if (encryptionKey === fallbackKey) { - logger.warn("Using a fallback encryption key, stored secrets are not secure"); - // We never want to use the fallback key in production - if (process.env.NODE_ENV === "production" && process.env.CI !== "true") { - throw new Error("Encryption key is not set"); - } -} - -const key = Buffer.from(encryptionKey, "hex"); +// We fallback to a key of 0s if the key was not provided because env validation was skipped +// This should only be the case in CI +const key = Buffer.from(env.SECRET_ENCRYPTION_KEY || "0".repeat(64), "hex"); export function encryptSecret(text: string): `${string}.${string}` { const initializationVector = crypto.randomBytes(16); diff --git a/scripts/generateRandomSecureKey.js b/scripts/generateRandomSecureKey.js deleted file mode 100644 index 4813ae6a5..000000000 --- a/scripts/generateRandomSecureKey.js +++ /dev/null @@ -1,7 +0,0 @@ -// This script generates a random secure key with a length of 64 characters -// This key is used to encrypt and decrypt the integration secrets for auth.js -// In production it is generated in run.sh and stored in the environment variables ENCRYPTION_KEY / AUTH_SECRET -// during runtime, it's also stored in a file. - -const crypto = require("crypto"); -console.log(crypto.randomBytes(32).toString("hex")); diff --git a/scripts/run.sh b/scripts/run.sh index 2ba89cdff..ccceda47b 100644 --- a/scripts/run.sh +++ b/scripts/run.sh @@ -6,31 +6,8 @@ else node ./db/migrations/$DB_DIALECT/migrate.cjs ./db/migrations/$DB_DIALECT fi -# Generates an encryption key if it doesn't exist and saves it to /secrets/encryptionKey -# Also sets the ENCRYPTION_KEY environment variable -encryptionKey="" -if [ -r /secrets/encryptionKey ]; then - echo "Encryption key already exists" - encryptionKey=$(cat /secrets/encryptionKey) -else - echo "Generating encryption key" - encryptionKey=$(node ./generateRandomSecureKey.js) - echo $encryptionKey > /secrets/encryptionKey -fi -export ENCRYPTION_KEY=$encryptionKey - -# Generates an auth secret if it doesn't exist and saves it to /secrets/authSecret -# Also sets the AUTH_SECRET environment variable required for auth.js -authSecret="" -if [ -r /secrets/authSecret ]; then - echo "Auth secret already exists" - authSecret=$(cat /secrets/authSecret) -else - echo "Generating auth secret" - authSecret=$(node ./generateRandomSecureKey.js) - echo $authSecret > /secrets/authSecret -fi -export AUTH_SECRET=$authSecret +# Auth secret is generated every time the container starts as it is required, but not used because we don't need JWTs or Mail hashing +export AUTH_SECRET=$(openssl rand -base64 32) # Start nginx proxy # 1. Replace the HOSTNAME in the nginx template file diff --git a/turbo.json b/turbo.json index b7c00eb22..b3732c66e 100644 --- a/turbo.json +++ b/turbo.json @@ -23,7 +23,6 @@ "AUTH_OIDC_AUTO_LOGIN", "AUTH_LOGOUT_REDIRECT_URL", "AUTH_PROVIDERS", - "AUTH_SECRET", "AUTH_SESSION_EXPIRY_TIME", "CI", "DISABLE_REDIS_LOGS", @@ -38,6 +37,7 @@ "DOCKER_PORTS", "NODE_ENV", "PORT", + "SECRET_ENCRYPTION_KEY", "SKIP_ENV_VALIDATION" ], "ui": "stream",