Explorar o código

refactor: protect redis instance with a password

Nicolas Meienberger hai 1 ano
pai
achega
e65461b146

+ 1 - 0
.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

+ 5 - 4
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

+ 0 - 121
docker-compose.e2e.yml

@@ -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

+ 0 - 106
docker-compose.rc.yml

@@ -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

+ 0 - 113
docker-compose.test.yml

@@ -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:

+ 8 - 5
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:

+ 2 - 2
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

+ 3 - 0
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');

+ 2 - 1
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', () => {

+ 3 - 1
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,
     };
   });

+ 1 - 0
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(),

+ 2 - 2
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() {

+ 1 - 0
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) => {

+ 1 - 0
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',

+ 1 - 0
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();