mirror of
https://github.com/ajnart/homarr.git
synced 2026-01-29 02:39:16 +01:00
fix(docker): replace anonymous docker volume with env variable for encrypting secrets (#1809)
This commit is contained in:
13
.env.example
13
.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
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -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;
|
||||
};
|
||||
|
||||
@@ -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,
|
||||
|
||||
28
packages/common/env.mjs
Normal file
28
packages/common/env.mjs
Normal file
@@ -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",
|
||||
});
|
||||
@@ -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": {
|
||||
"*": {
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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"));
|
||||
@@ -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
|
||||
|
||||
@@ -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",
|
||||
|
||||
Reference in New Issue
Block a user