Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions .dockerignore
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
node_modules
dist
.env
.env.dev
.env.prod
.env.*
!.env.example
.git
.gitignore
*.md
Expand Down
48 changes: 45 additions & 3 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,16 @@ LOG_LEVEL=info

# Database (PostgreSQL) - points to Docker container hostname
DATABASE_URL=postgresql://fluxcore:fluxcore@postgres:5432/fluxcore
# Optional dev overrides for the local postgres container — defaults shown
# POSTGRES_USER=fluxcore
# POSTGRES_PASSWORD=fluxcore
# POSTGRES_DB=fluxcore

# === Vite dev server (dashboard) ===
# Defaults to 127.0.0.1 (loopback only). Set to 0.0.0.0 ONLY when running
# inside Docker — exposing Vite to all interfaces on a bare-metal host is
# unsafe. The docker-compose dashboard service sets this automatically.
# VITE_HOST=0.0.0.0

# === Bot only ===

Expand All @@ -24,7 +34,9 @@ GUILD_ID=

# Port for the bot's internal HTTP cache sync server
BOT_SYNC_PORT=3001
# Shared secret for cache sync authentication (generate a random string)
# Shared secret for cache sync authentication.
# REQUIRED in production (NODE_ENV=production); auto-generated in development.
# Must be at least 32 characters. Generate with: openssl rand -hex 32
BOT_SYNC_SECRET=
# URL the dashboard uses to reach the bot's sync server (Docker service name)
BOT_SYNC_URL=http://bot:3001
Expand All @@ -43,8 +55,10 @@ DASHBOARD_SESSION_SECRET=
LAVALINK_HOST=lavalink
# Lavalink server port
LAVALINK_PORT=2333
# Lavalink server password (must match lavalink/application.yml)
LAVALINK_PASSWORD=youshallnotpass
# Lavalink server password (REQUIRED).
# Must match the LAVALINK_SERVER_PASSWORD env var the lavalink container reads.
# Generate with: openssl rand -base64 32
LAVALINK_PASSWORD=

# === Infrastructure (production only) ===

Expand All @@ -57,3 +71,31 @@ DASHBOARD_DOMAIN=localhost
# Backup schedule (cron expression, default: daily at 2am)
BACKUP_SCHEDULE=0 2 * * *
BACKUP_RETENTION_DAYS=7

# === Dev tools profile (pgadmin) ===
# Required when running: docker compose --profile tools up pgadmin
# PGADMIN_EMAIL=admin@fluxcore.local
# PGADMIN_PASSWORD=

# === Production secrets (Docker secrets — DO NOT commit values) ===
# In production, place each secret as a file under ./secrets/<name>:
# secrets/discord_token
# secrets/dashboard_client_secret
# secrets/dashboard_session_secret
# secrets/bot_sync_secret
# secrets/lavalink_password
# secrets/postgres_password
# Generate strong values with: openssl rand -base64 32
#
# The Node services read secrets via *_FILE env vars (see packages/config
# resolveSecretFiles). Example overrides if running outside docker-compose:
# DISCORD_TOKEN_FILE=/run/secrets/discord_token
# DASHBOARD_CLIENT_SECRET_FILE=/run/secrets/dashboard_client_secret
# DASHBOARD_SESSION_SECRET_FILE=/run/secrets/dashboard_session_secret
# BOT_SYNC_SECRET_FILE=/run/secrets/bot_sync_secret
# LAVALINK_PASSWORD_FILE=/run/secrets/lavalink_password
# POSTGRES_PASSWORD_FILE=/run/secrets/postgres_password
# DATABASE_PASSWORD_FILE=/run/secrets/postgres_password # alias for deployments
#
# If DATABASE_URL is unset, it is constructed at startup from POSTGRES_USER,
# POSTGRES_PASSWORD (or POSTGRES_PASSWORD_FILE), POSTGRES_HOST, POSTGRES_DB.
143 changes: 143 additions & 0 deletions .github/workflows/security.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,143 @@
name: Security

on:
push:
branches: [main]
pull_request:
schedule:
- cron: "0 6 * * 1" # Mondays 06:00 UTC

permissions:
contents: read
security-events: write

jobs:
pnpm-audit:
name: pnpm audit (high+)
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: pnpm/action-setup@v4
with:
version: 10.28.0
- uses: actions/setup-node@v4
with:
node-version: 22
cache: pnpm
- run: pnpm install --frozen-lockfile
- name: Audit (fail on high or critical)
run: pnpm audit --audit-level=high

gitleaks:
name: gitleaks
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Run gitleaks
uses: gitleaks/gitleaks-action@v2
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
GITLEAKS_ENABLE_UPLOAD_ARTIFACT: true

trivy-bot:
name: trivy (bot image)
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Build bot image
run: docker build --target production-bot -t fluxcore-bot:ci .
- name: Trivy scan
uses: aquasecurity/trivy-action@0.28.0
with:
image-ref: fluxcore-bot:ci
severity: HIGH,CRITICAL
exit-code: "1"
ignore-unfixed: true
vuln-type: os,library
format: sarif
output: trivy-bot.sarif
- name: Upload SARIF
if: always()
uses: github/codeql-action/upload-sarif@v3
with:
sarif_file: trivy-bot.sarif
category: trivy-bot

trivy-dashboard:
name: trivy (dashboard image)
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Build dashboard image
run: docker build --target production-dashboard -t fluxcore-dashboard:ci .
- name: Trivy scan
uses: aquasecurity/trivy-action@0.28.0
with:
image-ref: fluxcore-dashboard:ci
severity: HIGH,CRITICAL
exit-code: "1"
ignore-unfixed: true
vuln-type: os,library
format: sarif
output: trivy-dashboard.sarif
- name: Upload SARIF
if: always()
uses: github/codeql-action/upload-sarif@v3
with:
sarif_file: trivy-dashboard.sarif
category: trivy-dashboard

trivy-fs:
name: trivy (filesystem & config)
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Trivy filesystem scan (config, IaC, secrets)
uses: aquasecurity/trivy-action@0.28.0
with:
scan-type: fs
scan-ref: .
severity: HIGH,CRITICAL
exit-code: "1"
ignore-unfixed: true
scanners: vuln,secret,config

forbidden-strings:
name: forbidden strings
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Block re-introduction of default lavalink password
run: |
if grep -RIn --exclude-dir=.git --exclude-dir=node_modules --exclude='*.md' 'youshallnotpass' .; then
echo "::error::The literal 'youshallnotpass' must never be committed."
exit 1
fi
- name: Block public postgres exposure
run: |
if grep -RIn --include='docker-compose*.yml' -E '"(0\.0\.0\.0:)?5432:5432"' .; then
echo "::error::Postgres must only bind 127.0.0.1, never 0.0.0.0 or unbound."
exit 1
fi

no-env-files:
name: no env files in repo
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Assert no .env files are tracked
run: |
tracked=$(git ls-files | grep -E '^\.env(\..+)?$' | grep -v '^\.env\.example$' || true)
if [ -n "$tracked" ]; then
echo "::error::These env files must not be committed:"
echo "$tracked"
exit 1
fi
- name: Assert .dockerignore covers env files
run: |
grep -qE '^\.env\*?$|^\.env\.\*$' .dockerignore || {
echo "::error::.dockerignore must include '.env' and '.env.*'"
exit 1
}
7 changes: 5 additions & 2 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
node_modules/
dist/
.env
.env.dev
.env.prod
.env.*
!.env.example
*.js.map
*.d.ts
!src/**/*.d.ts
Expand All @@ -26,3 +26,6 @@ out/
apps/dashboard/tests/e2e/.auth/
# E2E HTML report
apps/dashboard/tests/e2e/.report/
# Production Docker secrets — keep the directory but never commit values
/secrets/*
!/secrets/.gitkeep
11 changes: 7 additions & 4 deletions .gitleaks.toml
Original file line number Diff line number Diff line change
@@ -1,11 +1,14 @@
title = "FluxCore Gitleaks Configuration"
title = "FluxCore gitleaks config"

[extend]
useDefault = true

[allowlist]
description = "Allow common false positives"
description = "Test fixtures and example placeholders"
paths = [
'''\.env\.example''',
'''.*\.test\.(ts|js)$''',
'''packages/systems/tests/.*''',
'''docs/.*''',
'''\.env\.example$''',
'''pnpm-lock\.yaml''',
]
]
9 changes: 6 additions & 3 deletions Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -20,14 +20,17 @@ COPY apps/bot/package.json ./apps/bot/
COPY apps/dashboard/package.json ./apps/dashboard/
COPY packages/database/prisma ./packages/database/prisma/
COPY packages/database/prisma.config.ts ./packages/database/
RUN pnpm install --frozen-lockfile
RUN pnpm install --frozen-lockfile \
&& chown -R node:node /app

FROM deps AS development
COPY . .
COPY --chown=node:node . .
USER node
CMD ["pnpm", "dev"]

FROM deps AS test
COPY . .
COPY --chown=node:node . .
USER node
CMD ["pnpm", "test"]

# ============================================
Expand Down
25 changes: 25 additions & 0 deletions apps/dashboard/tests/vite-config.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import { describe, it, expect } from "vitest";
import { readFileSync } from "node:fs";
import { fileURLToPath } from "node:url";
import { dirname, resolve } from "node:path";

// Source-level assertions on vite.config.ts. We can't load the config at
// runtime inside vitest (esbuild rejects the cache-busting query string),
// so we pin the host-binding logic by inspecting the source text.
describe("vite.config server.host", () => {
const here = dirname(fileURLToPath(import.meta.url));
const src = readFileSync(resolve(here, "../vite.config.ts"), "utf8");

it("defaults host to 127.0.0.1 (does NOT default to 0.0.0.0)", () => {
// Must reference VITE_HOST
expect(src).toContain("VITE_HOST");
// Must have 127.0.0.1 as the fallback
expect(src).toMatch(/VITE_HOST[^}]*127\.0\.0\.1/);
// Must NOT have 0.0.0.0 as a hardcoded default (only via env var)
expect(src).not.toMatch(/host:\s*["']0\.0\.0\.0["']/);
});

it("reads host from process.env.VITE_HOST", () => {
expect(src).toContain("process.env.VITE_HOST");
});
});
2 changes: 1 addition & 1 deletion apps/dashboard/vite.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ export default defineConfig({
},
server: {
port: 5173,
host: "0.0.0.0",
host: process.env.VITE_HOST ?? "127.0.0.1",
proxy: {
"/auth": {
target: "http://localhost:3000",
Expand Down
5 changes: 5 additions & 0 deletions docker-compose.no-db-port.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
# Apply with: docker compose -f docker-compose.yml -f docker-compose.no-db-port.yml up
# Suppresses the host loopback binding for the postgres service.
services:
postgres:
ports: !reset []
Loading
Loading