mirror of
https://github.com/ajnart/homarr.git
synced 2026-02-26 16:30:57 +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.
|
# 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.
|
# 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:
|
# This is how you can use the sqlite driver:
|
||||||
DB_DRIVER='better-sqlite3'
|
DB_DRIVER='better-sqlite3'
|
||||||
DB_URL='FULL_PATH_TO_YOUR_SQLITE_DB_FILE'
|
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_PASSWORD='password'
|
||||||
# DB_NAME='name-of-database'
|
# 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
|
TURBO_TELEMETRY_DISABLED=1
|
||||||
|
|
||||||
# Configure logging to use winston logger
|
# Configure logging to use winston logger
|
||||||
|
|||||||
@@ -25,12 +25,10 @@ RUN corepack enable pnpm && pnpm build
|
|||||||
FROM base AS runner
|
FROM base AS runner
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
|
|
||||||
# gettext is required for envsubst
|
# 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
|
RUN apk add --no-cache redis nginx bash gettext su-exec openssl
|
||||||
RUN mkdir /appdata
|
RUN mkdir /appdata
|
||||||
VOLUME /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
|
RUN chmod +x /usr/bin/homarr
|
||||||
|
|
||||||
# Don't run production as root
|
# 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 && \
|
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/log/nginx && chown -R nextjs:nodejs /var/log/nginx && \
|
||||||
mkdir -p /var/lib/nginx && chown -R nextjs:nodejs /var/lib/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 --chown=nextjs:nodejs scripts/run.sh ./run.sh
|
||||||
COPY scripts/entrypoint.sh ./entrypoint.sh
|
COPY scripts/entrypoint.sh ./entrypoint.sh
|
||||||
RUN chmod +x ./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 packages/redis/redis.conf /app/redis.conf
|
||||||
COPY --chown=nextjs:nodejs nginx.conf /etc/nginx/templates/nginx.conf
|
COPY --chown=nextjs:nodejs nginx.conf /etc/nginx/templates/nginx.conf
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
// Importing env files here to validate on build
|
// Importing env files here to validate on build
|
||||||
import "@homarr/auth/env.mjs";
|
import "@homarr/auth/env.mjs";
|
||||||
import "@homarr/db/env.mjs";
|
import "@homarr/db/env.mjs";
|
||||||
|
import "@homarr/common/env.mjs";
|
||||||
|
|
||||||
import MillionLint from "@million/lint";
|
import MillionLint from "@million/lint";
|
||||||
import createNextIntlPlugin from "next-intl/plugin";
|
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'");
|
throw new Error("This test should only be run in CI or with a homarr image named 'homarr-e2e'");
|
||||||
}
|
}
|
||||||
|
|
||||||
return new GenericContainer("homarr-e2e")
|
return withLogs(
|
||||||
.withExposedPorts(7575)
|
new GenericContainer("homarr-e2e")
|
||||||
.withWaitStrategy(Wait.forHttp("/api/health/ready", 7575));
|
.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: {
|
server: {
|
||||||
AUTH_LOGOUT_REDIRECT_URL: z.string().url().optional(),
|
AUTH_LOGOUT_REDIRECT_URL: z.string().url().optional(),
|
||||||
AUTH_SESSION_EXPIRY_TIME: createDurationSchema("30d"),
|
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,
|
AUTH_PROVIDERS: authProvidersSchema,
|
||||||
...(authProviders.includes("oidc")
|
...(authProviders.includes("oidc")
|
||||||
? {
|
? {
|
||||||
@@ -98,7 +97,6 @@ export const env = createEnv({
|
|||||||
runtimeEnv: {
|
runtimeEnv: {
|
||||||
AUTH_LOGOUT_REDIRECT_URL: process.env.AUTH_LOGOUT_REDIRECT_URL,
|
AUTH_LOGOUT_REDIRECT_URL: process.env.AUTH_LOGOUT_REDIRECT_URL,
|
||||||
AUTH_SESSION_EXPIRY_TIME: process.env.AUTH_SESSION_EXPIRY_TIME,
|
AUTH_SESSION_EXPIRY_TIME: process.env.AUTH_SESSION_EXPIRY_TIME,
|
||||||
AUTH_SECRET: process.env.AUTH_SECRET,
|
|
||||||
AUTH_PROVIDERS: process.env.AUTH_PROVIDERS,
|
AUTH_PROVIDERS: process.env.AUTH_PROVIDERS,
|
||||||
AUTH_LDAP_BASE: process.env.AUTH_LDAP_BASE,
|
AUTH_LDAP_BASE: process.env.AUTH_LDAP_BASE,
|
||||||
AUTH_LDAP_BIND_DN: process.env.AUTH_LDAP_BIND_DN,
|
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",
|
".": "./index.ts",
|
||||||
"./types": "./src/types.ts",
|
"./types": "./src/types.ts",
|
||||||
"./server": "./src/server.ts",
|
"./server": "./src/server.ts",
|
||||||
"./client": "./src/client.ts"
|
"./client": "./src/client.ts",
|
||||||
|
"./env.mjs": "./env.mjs"
|
||||||
},
|
},
|
||||||
"typesVersions": {
|
"typesVersions": {
|
||||||
"*": {
|
"*": {
|
||||||
|
|||||||
@@ -1,20 +1,12 @@
|
|||||||
import crypto from "crypto";
|
import crypto from "crypto";
|
||||||
|
|
||||||
import { logger } from "@homarr/log";
|
import { env } from "../env.mjs";
|
||||||
|
|
||||||
const algorithm = "aes-256-cbc"; //Using AES encryption
|
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
|
// We fallback to a key of 0s if the key was not provided because env validation was skipped
|
||||||
if (process.env.NODE_ENV === "production" && process.env.CI !== "true") {
|
// This should only be the case in CI
|
||||||
throw new Error("Encryption key is not set");
|
const key = Buffer.from(env.SECRET_ENCRYPTION_KEY || "0".repeat(64), "hex");
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const key = Buffer.from(encryptionKey, "hex");
|
|
||||||
|
|
||||||
export function encryptSecret(text: string): `${string}.${string}` {
|
export function encryptSecret(text: string): `${string}.${string}` {
|
||||||
const initializationVector = crypto.randomBytes(16);
|
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
|
node ./db/migrations/$DB_DIALECT/migrate.cjs ./db/migrations/$DB_DIALECT
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# Generates an encryption key if it doesn't exist and saves it to /secrets/encryptionKey
|
# 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
|
||||||
# Also sets the ENCRYPTION_KEY environment variable
|
export AUTH_SECRET=$(openssl rand -base64 32)
|
||||||
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
|
|
||||||
|
|
||||||
# Start nginx proxy
|
# Start nginx proxy
|
||||||
# 1. Replace the HOSTNAME in the nginx template file
|
# 1. Replace the HOSTNAME in the nginx template file
|
||||||
|
|||||||
@@ -23,7 +23,6 @@
|
|||||||
"AUTH_OIDC_AUTO_LOGIN",
|
"AUTH_OIDC_AUTO_LOGIN",
|
||||||
"AUTH_LOGOUT_REDIRECT_URL",
|
"AUTH_LOGOUT_REDIRECT_URL",
|
||||||
"AUTH_PROVIDERS",
|
"AUTH_PROVIDERS",
|
||||||
"AUTH_SECRET",
|
|
||||||
"AUTH_SESSION_EXPIRY_TIME",
|
"AUTH_SESSION_EXPIRY_TIME",
|
||||||
"CI",
|
"CI",
|
||||||
"DISABLE_REDIS_LOGS",
|
"DISABLE_REDIS_LOGS",
|
||||||
@@ -38,6 +37,7 @@
|
|||||||
"DOCKER_PORTS",
|
"DOCKER_PORTS",
|
||||||
"NODE_ENV",
|
"NODE_ENV",
|
||||||
"PORT",
|
"PORT",
|
||||||
|
"SECRET_ENCRYPTION_KEY",
|
||||||
"SKIP_ENV_VALIDATION"
|
"SKIP_ENV_VALIDATION"
|
||||||
],
|
],
|
||||||
"ui": "stream",
|
"ui": "stream",
|
||||||
|
|||||||
Reference in New Issue
Block a user