Skip to content

Commit

Permalink
fix(docker): replace anonymous docker volume with env variable for en…
Browse files Browse the repository at this point in the history
…crypting secrets (#1809)
  • Loading branch information
Meierschlumpf authored Dec 31, 2024
1 parent 91e1cef commit 56b57ad
Show file tree
Hide file tree
Showing 11 changed files with 66 additions and 62 deletions.
13 changes: 8 additions & 5 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand All @@ -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
Expand Down
8 changes: 2 additions & 6 deletions Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -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



Expand All @@ -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 && \
Expand All @@ -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

Expand Down
1 change: 1 addition & 0 deletions apps/nextjs/next.config.mjs
Original file line number Diff line number Diff line change
@@ -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";
Expand Down
21 changes: 18 additions & 3 deletions e2e/shared/create-homarr-container.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
};
2 changes: 0 additions & 2 deletions packages/auth/env.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -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")
? {
Expand Down Expand Up @@ -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,
Expand Down
28 changes: 28 additions & 0 deletions packages/common/env.mjs
Original file line number Diff line number Diff line change
@@ -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",
});
3 changes: 2 additions & 1 deletion packages/common/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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": {
"*": {
Expand Down
16 changes: 4 additions & 12 deletions packages/common/src/encryption.ts
Original file line number Diff line number Diff line change
@@ -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);
Expand Down
7 changes: 0 additions & 7 deletions scripts/generateRandomSecureKey.js

This file was deleted.

27 changes: 2 additions & 25 deletions scripts/run.sh
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion turbo.json
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,6 @@
"AUTH_OIDC_AUTO_LOGIN",
"AUTH_LOGOUT_REDIRECT_URL",
"AUTH_PROVIDERS",
"AUTH_SECRET",
"AUTH_SESSION_EXPIRY_TIME",
"CI",
"DISABLE_REDIS_LOGS",
Expand All @@ -38,6 +37,7 @@
"DOCKER_PORTS",
"NODE_ENV",
"PORT",
"SECRET_ENCRYPTION_KEY",
"SKIP_ENV_VALIDATION"
],
"ui": "stream",
Expand Down

0 comments on commit 56b57ad

Please sign in to comment.