From e65461b1463468d3410dd7383bbb7718c7d5894d Mon Sep 17 00:00:00 2001 From: Nicolas Meienberger Date: Thu, 24 Aug 2023 18:40:38 +0200 Subject: [PATCH] refactor: protect redis instance with a password --- .env.example | 1 + docker-compose.dev.yml | 9 +- docker-compose.e2e.yml | 121 ------------------ docker-compose.rc.yml | 106 --------------- docker-compose.test.yml | 113 ---------------- packages/cli/assets/docker-compose.yml | 13 +- .../src/executors/system/system.executors.ts | 4 +- .../src/executors/system/system.helpers.ts | 3 + packages/cli/src/services/watcher/watcher.ts | 3 +- .../cli/src/utils/environment/environment.ts | 4 +- packages/shared/src/schemas/env-schemas.ts | 1 + .../core/EventDispatcher/EventDispatcher.ts | 4 +- src/server/core/TipiCache/TipiCache.ts | 1 + src/server/core/TipiConfig/TipiConfig.ts | 1 + src/server/middlewares/session.middleware.ts | 1 + 15 files changed, 30 insertions(+), 355 deletions(-) delete mode 100644 docker-compose.e2e.yml delete mode 100644 docker-compose.rc.yml delete mode 100644 docker-compose.test.yml diff --git a/.env.example b/.env.example index a73b69f4..66d6acda 100644 --- a/.env.example +++ b/.env.example @@ -17,5 +17,6 @@ POSTGRES_USERNAME=tipi POSTGRES_PASSWORD=postgres POSTGRES_PORT=5432 REDIS_HOST=tipi-redis +REDIS_PASSWORD=redis DEMO_MODE=false LOCAL_DOMAIN=tipi.lan diff --git a/docker-compose.dev.yml b/docker-compose.dev.yml index 19e9a78a..d96b3555 100644 --- a/docker-compose.dev.yml +++ b/docker-compose.dev.yml @@ -1,8 +1,8 @@ version: '3.7' services: - reverse-proxy: - container_name: reverse-proxy + tipi-reverse-proxy: + container_name: tipi-reverse-proxy image: traefik:v2.8 restart: on-failure ports: @@ -42,6 +42,7 @@ services: container_name: tipi-redis image: redis:alpine restart: unless-stopped + command: redis-server --requirepass ${REDIS_PASSWORD} ports: - 6379:6379 volumes: @@ -54,11 +55,11 @@ services: networks: - tipi_main_network - dashboard: + tipi-dashboard: build: context: . dockerfile: Dockerfile.dev - container_name: dashboard + container_name: tipi-dashboard depends_on: tipi-db: condition: service_healthy diff --git a/docker-compose.e2e.yml b/docker-compose.e2e.yml deleted file mode 100644 index 90bdf22a..00000000 --- a/docker-compose.e2e.yml +++ /dev/null @@ -1,121 +0,0 @@ -version: '3.7' - -services: - reverse-proxy: - container_name: reverse-proxy - image: traefik:v2.8 - restart: unless-stopped - ports: - - ${NGINX_PORT-80}:80 - - ${NGINX_PORT_SSL-443}:443 - command: --providers.docker - volumes: - - /var/run/docker.sock:/var/run/docker.sock:ro - - ${PWD}/traefik:/root/.config - - ${PWD}/traefik/shared:/shared - networks: - - tipi_main_network - - tipi-db: - container_name: tipi-db - image: postgres:14 - restart: unless-stopped - stop_grace_period: 1m - ports: - - 5432:5432 - environment: - POSTGRES_PASSWORD: ${POSTGRES_PASSWORD} - POSTGRES_USER: tipi - POSTGRES_DB: tipi - healthcheck: - test: ['CMD-SHELL', 'pg_isready -d tipi -U tipi'] - interval: 5s - timeout: 10s - retries: 120 - networks: - - tipi_main_network - - tipi-redis: - container_name: tipi-redis - image: redis:alpine - restart: unless-stopped - volumes: - - ./data/redis:/data - healthcheck: - test: ['CMD', 'redis-cli', 'ping'] - interval: 5s - timeout: 10s - retries: 120 - networks: - - tipi_main_network - - dashboard: - image: meienberger/runtipi:${DOCKER_TAG} - restart: unless-stopped - container_name: dashboard - networks: - - tipi_main_network - depends_on: - tipi-db: - condition: service_healthy - tipi-redis: - condition: service_healthy - environment: - NODE_ENV: production - INTERNAL_IP: ${INTERNAL_IP} - TIPI_VERSION: ${TIPI_VERSION} - JWT_SECRET: ${JWT_SECRET} - NGINX_PORT: ${NGINX_PORT} - POSTGRES_PASSWORD: ${POSTGRES_PASSWORD} - POSTGRES_USERNAME: ${POSTGRES_USERNAME} - POSTGRES_DBNAME: ${POSTGRES_DBNAME} - POSTGRES_HOST: ${POSTGRES_HOST} - APPS_REPO_ID: ${APPS_REPO_ID} - APPS_REPO_URL: ${APPS_REPO_URL} - DOMAIN: ${DOMAIN} - ARCHITECTURE: ${ARCHITECTURE} - REDIS_HOST: ${REDIS_HOST} - DEMO_MODE: ${DEMO_MODE} - LOCAL_DOMAIN: ${LOCAL_DOMAIN} - volumes: - - ${PWD}/state:/runtipi/state - - ${PWD}/repos:/runtipi/repos:ro - - ${PWD}/apps:/runtipi/apps - - ${PWD}/logs:/app/logs - - ${PWD}/traefik:/runtipi/traefik - - ${PWD}:/app/storage - labels: - # Main - traefik.enable: true - traefik.http.middlewares.redirect-to-https.redirectscheme.scheme: https - traefik.http.services.dashboard.loadbalancer.server.port: 3000 - # Local ip - traefik.http.routers.dashboard.rule: PathPrefix("/") - traefik.http.routers.dashboard.service: dashboard - traefik.http.routers.dashboard.entrypoints: web - # Websecure - traefik.http.routers.dashboard-insecure.rule: Host(`${DOMAIN}`) && PathPrefix(`/`) - traefik.http.routers.dashboard-insecure.service: dashboard - traefik.http.routers.dashboard-insecure.entrypoints: web - traefik.http.routers.dashboard-insecure.middlewares: redirect-to-https - traefik.http.routers.dashboard-secure.rule: Host(`${DOMAIN}`) && PathPrefix(`/`) - traefik.http.routers.dashboard-secure.service: dashboard - traefik.http.routers.dashboard-secure.entrypoints: websecure - traefik.http.routers.dashboard-secure.tls.certresolver: myresolver - # Local domain - traefik.http.routers.dashboard-local-insecure.rule: Host(`${LOCAL_DOMAIN}`) - traefik.http.routers.dashboard-local-insecure.entrypoints: web - traefik.http.routers.dashboard-local-insecure.service: dashboard - traefik.http.routers.dashboard-local-insecure.middlewares: redirect-to-https - traefik.http.routers.dashboard-local.rule: Host(`${LOCAL_DOMAIN}`) - traefik.http.routers.dashboard-local.entrypoints: websecure - traefik.http.routers.dashboard-local.tls: true - traefik.http.routers.dashboard-local.service: dashboard - -networks: - tipi_main_network: - driver: bridge - ipam: - driver: default - config: - - subnet: 10.21.21.0/24 diff --git a/docker-compose.rc.yml b/docker-compose.rc.yml deleted file mode 100644 index 9805c6dd..00000000 --- a/docker-compose.rc.yml +++ /dev/null @@ -1,106 +0,0 @@ -version: '3.7' - -services: - reverse-proxy: - container_name: reverse-proxy - image: traefik:v2.8 - restart: unless-stopped - ports: - - ${NGINX_PORT-80}:80 - - ${NGINX_PORT_SSL-443}:443 - - 8080:8080 - command: --providers.docker - volumes: - - /var/run/docker.sock:/var/run/docker.sock:ro - - ${PWD}/traefik:/root/.config - - ${PWD}/traefik/shared:/shared - networks: - - tipi_main_network - - tipi-db: - container_name: tipi-db - image: postgres:14 - restart: unless-stopped - stop_grace_period: 1m - volumes: - - ./data/postgres:/var/lib/postgresql/data - environment: - POSTGRES_PASSWORD: ${POSTGRES_PASSWORD} - POSTGRES_USER: tipi - POSTGRES_DB: tipi - healthcheck: - test: ['CMD-SHELL', 'pg_isready -d tipi -U tipi'] - interval: 5s - timeout: 10s - retries: 120 - networks: - - tipi_main_network - - tipi-redis: - container_name: tipi-redis - image: redis:alpine - restart: unless-stopped - volumes: - - ./data/redis:/data - healthcheck: - test: ['CMD', 'redis-cli', 'ping'] - interval: 5s - timeout: 10s - retries: 120 - networks: - - tipi_main_network - - dashboard: - image: meienberger/runtipi:rc-${TIPI_VERSION} - container_name: dashboard - networks: - - tipi_main_network - depends_on: - tipi-db: - condition: service_healthy - tipi-redis: - condition: service_healthy - environment: - NODE_ENV: production - INTERNAL_IP: ${INTERNAL_IP} - TIPI_VERSION: ${TIPI_VERSION} - JWT_SECRET: ${JWT_SECRET} - NGINX_PORT: ${NGINX_PORT} - POSTGRES_PASSWORD: ${POSTGRES_PASSWORD} - POSTGRES_USERNAME: ${POSTGRES_USERNAME} - POSTGRES_DBNAME: ${POSTGRES_DBNAME} - POSTGRES_HOST: ${POSTGRES_HOST} - APPS_REPO_ID: ${APPS_REPO_ID} - APPS_REPO_URL: ${APPS_REPO_URL} - DOMAIN: ${DOMAIN} - ARCHITECTURE: ${ARCHITECTURE} - REDIS_HOST: ${REDIS_HOST} - DEMO_MODE: ${DEMO_MODE} - volumes: - - ${PWD}/.env:/runtipi/.env - - ${PWD}/state:/runtipi/state - - ${PWD}/repos:/runtipi/repos:ro - - ${PWD}/apps:/runtipi/apps - - ${PWD}/logs:/app/logs - - ${STORAGE_PATH}:/app/storage - labels: - traefik.enable: true - # Web - traefik.http.routers.dashboard.rule: PathPrefix("/") - traefik.http.routers.dashboard.service: dashboard - traefik.http.routers.dashboard.entrypoints: web - traefik.http.services.dashboard.loadbalancer.server.port: 3000 - # Websecure - traefik.http.routers.dashboard-secure.rule: Host(`${DOMAIN}`) && PathPrefix(`/`) - traefik.http.routers.dashboard-secure.service: dashboard-secure - traefik.http.routers.dashboard-secure.entrypoints: websecure - traefik.http.routers.dashboard-secure.tls.certresolver: myresolver - traefik.http.services.dashboard-secure.loadbalancer.server.port: 3000 - -networks: - tipi_main_network: - driver: bridge - ipam: - driver: default - config: - - subnet: 10.21.21.0/24 diff --git a/docker-compose.test.yml b/docker-compose.test.yml deleted file mode 100644 index 2923e1d1..00000000 --- a/docker-compose.test.yml +++ /dev/null @@ -1,113 +0,0 @@ -version: '3.7' - -services: - reverse-proxy: - container_name: reverse-proxy - image: traefik:v2.8 - restart: unless-stopped - ports: - - ${NGINX_PORT-80}:80 - - ${NGINX_PORT_SSL-443}:443 - command: --providers.docker - volumes: - - /var/run/docker.sock:/var/run/docker.sock:ro - - ${PWD}/traefik:/root/.config - - ${PWD}/traefik/shared:/shared - networks: - - tipi_main_network - - tipi-db: - container_name: tipi-db - image: postgres:14 - restart: unless-stopped - stop_grace_period: 1m - ports: - - 5432:5432 - volumes: - - pgdata:/var/lib/postgresql/data - environment: - POSTGRES_PASSWORD: ${POSTGRES_PASSWORD} - POSTGRES_USER: tipi - POSTGRES_DB: tipi - healthcheck: - test: ['CMD-SHELL', 'pg_isready -d tipi -U tipi'] - interval: 5s - timeout: 10s - retries: 120 - networks: - - tipi_main_network - - tipi-redis: - container_name: tipi-redis - image: redis:alpine - restart: unless-stopped - volumes: - - ./data/redis:/data - healthcheck: - test: ['CMD', 'redis-cli', 'ping'] - interval: 5s - timeout: 10s - retries: 120 - networks: - - tipi_main_network - - dashboard: - build: - context: . - dockerfile: Dockerfile - restart: unless-stopped - container_name: dashboard - networks: - - tipi_main_network - depends_on: - tipi-db: - condition: service_healthy - tipi-redis: - condition: service_healthy - environment: - NODE_ENV: production - INTERNAL_IP: ${INTERNAL_IP} - TIPI_VERSION: ${TIPI_VERSION} - JWT_SECRET: ${JWT_SECRET} - NGINX_PORT: ${NGINX_PORT} - POSTGRES_PASSWORD: ${POSTGRES_PASSWORD} - POSTGRES_USERNAME: ${POSTGRES_USERNAME} - POSTGRES_DBNAME: ${POSTGRES_DBNAME} - POSTGRES_HOST: ${POSTGRES_HOST} - APPS_REPO_ID: ${APPS_REPO_ID} - APPS_REPO_URL: ${APPS_REPO_URL} - DOMAIN: ${DOMAIN} - ARCHITECTURE: ${ARCHITECTURE} - REDIS_HOST: ${REDIS_HOST} - DEMO_MODE: ${DEMO_MODE} - volumes: - - ${PWD}/.env:/runtipi/.env - - ${PWD}/state:/runtipi/state - - ${PWD}/repos:/runtipi/repos:ro - - ${PWD}/apps:/runtipi/apps - - ${PWD}/logs:/app/logs - - ${STORAGE_PATH}:/app/storage - labels: - traefik.enable: true - # Web - traefik.http.routers.dashboard.rule: PathPrefix("/") - traefik.http.routers.dashboard.service: dashboard - traefik.http.routers.dashboard.entrypoints: web - traefik.http.services.dashboard.loadbalancer.server.port: 3000 - # Websecure - traefik.http.routers.dashboard-secure.rule: Host(`${DOMAIN}`) && PathPrefix(`/`) - traefik.http.routers.dashboard-secure.service: dashboard-secure - traefik.http.routers.dashboard-secure.entrypoints: websecure - traefik.http.routers.dashboard-secure.tls.certresolver: myresolver - traefik.http.services.dashboard-secure.loadbalancer.server.port: 3000 - -networks: - tipi_main_network: - driver: bridge - ipam: - driver: default - config: - - subnet: 10.21.21.0/24 - -volumes: - pgdata: diff --git a/packages/cli/assets/docker-compose.yml b/packages/cli/assets/docker-compose.yml index f2dea63c..19159488 100644 --- a/packages/cli/assets/docker-compose.yml +++ b/packages/cli/assets/docker-compose.yml @@ -1,8 +1,8 @@ version: '3.7' services: - reverse-proxy: - container_name: reverse-proxy + tipi-reverse-proxy: + container_name: tipi-reverse-proxy image: traefik:v2.8 restart: on-failure ports: @@ -21,6 +21,8 @@ services: image: postgres:14 restart: on-failure stop_grace_period: 1m + ports: + - 5432:5432 volumes: - ./data/postgres:/var/lib/postgresql/data environment: @@ -37,8 +39,9 @@ services: tipi-redis: container_name: tipi-redis - image: redis:alpine + image: redis:7.2.0-alpine restart: on-failure + command: redis-server --requirepass ${REDIS_PASSWORD} ports: - 6379:6379 volumes: @@ -51,10 +54,10 @@ services: networks: - tipi_main_network - dashboard: + tipi-dashboard: image: meienberger/runtipi:${TIPI_VERSION} restart: on-failure - container_name: dashboard + container_name: tipi-dashboard networks: - tipi_main_network depends_on: diff --git a/packages/cli/src/executors/system/system.executors.ts b/packages/cli/src/executors/system/system.executors.ts index 588177dd..64d9fb52 100644 --- a/packages/cli/src/executors/system/system.executors.ts +++ b/packages/cli/src/executors/system/system.executors.ts @@ -159,8 +159,8 @@ export class SystemExecutors { spinner.start(); await execAsync('docker rm -f tipi-db'); await execAsync('docker rm -f tipi-redis'); - await execAsync('docker rm -f dashboard'); - await execAsync('docker rm -f reverse-proxy'); + await execAsync('docker rm -f tipi-dashboard'); + await execAsync('docker rm -f tipi-reverse-proxy'); spinner.done('Containers stopped and removed'); // Pull images diff --git a/packages/cli/src/executors/system/system.helpers.ts b/packages/cli/src/executors/system/system.helpers.ts index e968b275..849415dc 100644 --- a/packages/cli/src/executors/system/system.helpers.ts +++ b/packages/cli/src/executors/system/system.helpers.ts @@ -30,6 +30,7 @@ type EnvKeys = | 'POSTGRES_PASSWORD' | 'POSTGRES_USERNAME' | 'REDIS_HOST' + | 'REDIS_PASSWORD' | 'LOCAL_DOMAIN' | 'DEMO_MODE' // eslint-disable-next-line @typescript-eslint/ban-types @@ -148,6 +149,7 @@ export const generateSystemEnvFile = async () => { const jwtSecret = envMap.get('JWT_SECRET') || (await deriveEntropy('jwt_secret')); const repoId = getRepoHash(data.appsRepoUrl || DEFAULT_REPO_URL); const postgresPassword = envMap.get('POSTGRES_PASSWORD') || (await deriveEntropy('postgres_password')); + const redisPassword = envMap.get('REDIS_PASSWORD') || (await deriveEntropy('redis_password')); const version = await fs.promises.readFile(path.join(rootFolder, 'VERSION'), 'utf-8'); @@ -170,6 +172,7 @@ export const generateSystemEnvFile = async () => { envMap.set('POSTGRES_PASSWORD', postgresPassword); envMap.set('POSTGRES_PORT', String(5432)); envMap.set('REDIS_HOST', 'tipi-redis'); + envMap.set('REDIS_PASSWORD', redisPassword); envMap.set('DEMO_MODE', String(data.demoMode || 'false')); envMap.set('LOCAL_DOMAIN', data.localDomain || 'tipi.lan'); envMap.set('NODE_ENV', 'production'); diff --git a/packages/cli/src/services/watcher/watcher.ts b/packages/cli/src/services/watcher/watcher.ts index 9e4b39bd..2614ea18 100644 --- a/packages/cli/src/services/watcher/watcher.ts +++ b/packages/cli/src/services/watcher/watcher.ts @@ -3,6 +3,7 @@ import { Worker } from 'bullmq'; import { exec } from 'child_process'; import { promisify } from 'util'; import { AppExecutors, RepoExecutors, SystemExecutors } from '@/executors'; +import { getEnv } from '@/utils/environment/environment'; const execAsync = promisify(exec); @@ -101,7 +102,7 @@ export const startWorker = async () => { return { success, stdout: message }; }, - { connection: { host: '127.0.0.1', port: 6379 } }, + { connection: { host: '127.0.0.1', port: 6379, password: getEnv().redisPassword } }, ); worker.on('ready', () => { diff --git a/packages/cli/src/utils/environment/environment.ts b/packages/cli/src/utils/environment/environment.ts index 6ec58fb2..d9ae4d12 100644 --- a/packages/cli/src/utils/environment/environment.ts +++ b/packages/cli/src/utils/environment/environment.ts @@ -15,9 +15,10 @@ const environmentSchema = z ARCHITECTURE: z.enum(['arm64', 'amd64']), INTERNAL_IP: z.string().ip().or(z.literal('localhost')), TIPI_VERSION: z.string(), + REDIS_PASSWORD: z.string(), }) .transform((env) => { - const { STORAGE_PATH, ARCHITECTURE, ROOT_FOLDER_HOST, APPS_REPO_ID, INTERNAL_IP, TIPI_VERSION, ...rest } = env; + const { STORAGE_PATH, ARCHITECTURE, ROOT_FOLDER_HOST, APPS_REPO_ID, INTERNAL_IP, TIPI_VERSION, REDIS_PASSWORD, ...rest } = env; return { storagePath: STORAGE_PATH, @@ -26,6 +27,7 @@ const environmentSchema = z arch: ARCHITECTURE, tipiVersion: TIPI_VERSION, internalIp: INTERNAL_IP, + redisPassword: REDIS_PASSWORD, ...rest, }; }); diff --git a/packages/shared/src/schemas/env-schemas.ts b/packages/shared/src/schemas/env-schemas.ts index 85c31293..bbf031e1 100644 --- a/packages/shared/src/schemas/env-schemas.ts +++ b/packages/shared/src/schemas/env-schemas.ts @@ -10,6 +10,7 @@ export type Architecture = (typeof ARCHITECTURES)[keyof typeof ARCHITECTURES]; export const envSchema = z.object({ NODE_ENV: z.union([z.literal('development'), z.literal('production'), z.literal('test')]), REDIS_HOST: z.string(), + redisPassword: z.string(), status: z.union([z.literal('RUNNING'), z.literal('UPDATING'), z.literal('RESTARTING')]), architecture: z.nativeEnum(ARCHITECTURES), dnsIp: z.string().ip().trim(), diff --git a/src/server/core/EventDispatcher/EventDispatcher.ts b/src/server/core/EventDispatcher/EventDispatcher.ts index 26052b9a..446cbf4c 100644 --- a/src/server/core/EventDispatcher/EventDispatcher.ts +++ b/src/server/core/EventDispatcher/EventDispatcher.ts @@ -17,8 +17,8 @@ class EventDispatcher { private queueEvents; constructor() { - this.queue = new Queue('events', { connection: { host: getConfig().REDIS_HOST, port: 6379 } }); - this.queueEvents = new QueueEvents('events', { connection: { host: getConfig().REDIS_HOST, port: 6379 } }); + this.queue = new Queue('events', { connection: { host: getConfig().REDIS_HOST, port: 6379, password: getConfig().redisPassword } }); + this.queueEvents = new QueueEvents('events', { connection: { host: getConfig().REDIS_HOST, port: 6379, password: getConfig().redisPassword } }); } public async cleanRepeatableJobs() { diff --git a/src/server/core/TipiCache/TipiCache.ts b/src/server/core/TipiCache/TipiCache.ts index f9f11410..1111ffae 100644 --- a/src/server/core/TipiCache/TipiCache.ts +++ b/src/server/core/TipiCache/TipiCache.ts @@ -12,6 +12,7 @@ class TipiCache { constructor() { const client = createClient({ url: `redis://${getConfig().REDIS_HOST}:6379`, + password: getConfig().redisPassword, }); client.on('error', (err) => { diff --git a/src/server/core/TipiConfig/TipiConfig.ts b/src/server/core/TipiConfig/TipiConfig.ts index 03423583..2222cc0b 100644 --- a/src/server/core/TipiConfig/TipiConfig.ts +++ b/src/server/core/TipiConfig/TipiConfig.ts @@ -27,6 +27,7 @@ export class TipiConfig { postgresPassword: conf.POSTGRES_PASSWORD, postgresPort: Number(conf.POSTGRES_PORT || 5432), REDIS_HOST: conf.REDIS_HOST, + redisPassword: conf.REDIS_PASSWORD, NODE_ENV: conf.NODE_ENV, architecture: conf.ARCHITECTURE || 'amd64', rootFolder: '/runtipi', diff --git a/src/server/middlewares/session.middleware.ts b/src/server/middlewares/session.middleware.ts index d15bc5e8..6d5a3e7b 100644 --- a/src/server/middlewares/session.middleware.ts +++ b/src/server/middlewares/session.middleware.ts @@ -6,6 +6,7 @@ import { getConfig } from '../core/TipiConfig'; // Initialize client. const redisClient = createClient({ url: `redis://${getConfig().REDIS_HOST}:6379`, + password: getConfig().redisPassword, }); redisClient.connect();