mirror of
https://github.com/BradGroux/veritas-kanban.git
synced 2026-08-28 02:44:59 +00:00
fix(security): remove .env from git, add .env.example
This commit is contained in:
parent
0c42d0b125
commit
5aa31115ae
13 changed files with 210 additions and 28 deletions
5
.gitignore
vendored
5
.gitignore
vendored
|
|
@ -21,10 +21,13 @@ Thumbs.db
|
|||
npm-debug.log*
|
||||
pnpm-debug.log*
|
||||
|
||||
# Env
|
||||
# Env — never commit real secrets
|
||||
.env
|
||||
.env.local
|
||||
.env.*.local
|
||||
# But DO track .env.example files
|
||||
!*.env.example
|
||||
!.env.example
|
||||
|
||||
# Task data (track structure, not content)
|
||||
tasks/active/*.md
|
||||
|
|
|
|||
|
|
@ -1725,3 +1725,6 @@
|
|||
{"id": "evt_yXsmTC6q2-ah", "type": "run.started", "timestamp": "2026-01-28T17:24:18.997Z", "taskId": "task_20260128_frXVnb", "project": "veritas-kanban", "agent": "veritas", "model": "claude-opus-4-5", "attemptId": "attempt_8iHDP2NJ"}
|
||||
{"id": "evt_OSNLg9gwRczN", "type": "run.tokens", "timestamp": "2026-01-28T17:24:18.997Z", "taskId": "task_20260128_frXVnb", "project": "veritas-kanban", "agent": "veritas", "model": "claude-opus-4-5", "attemptId": "attempt_8iHDP2NJ", "inputTokens": 15150, "outputTokens": 3072, "cacheTokens": 6592, "totalTokens": 18222, "cost": 0.4577}
|
||||
{"id": "evt_o08MkHms6jGB", "type": "run.completed", "timestamp": "2026-01-28T17:24:18.997Z", "taskId": "task_20260128_frXVnb", "project": "veritas-kanban", "agent": "veritas", "attemptId": "attempt_8iHDP2NJ", "success": true, "durationMs": 201000}
|
||||
{"type":"task.status_changed","taskId":"task_20260128_u73mT3","project":"veritas-kanban","status":"in-progress","previousStatus":"todo","id":"evt_EjShEPw7YS3u","timestamp":"2026-01-28T18:04:06.746Z"}
|
||||
{"type":"task.status_changed","taskId":"task_20260128_VVqEG1","project":"veritas-kanban","status":"in-progress","previousStatus":"todo","id":"evt_X4p02S1uaswP","timestamp":"2026-01-28T18:04:06.932Z"}
|
||||
{"type":"task.status_changed","taskId":"task_20260128_xu0DY5","project":"veritas-kanban","status":"in-progress","previousStatus":"todo","id":"evt_j4vRMJuwSzya","timestamp":"2026-01-28T18:04:07.737Z"}
|
||||
|
|
|
|||
|
|
@ -48,6 +48,13 @@ A local-first task management and AI agent orchestration platform. Built for dev
|
|||
git clone https://github.com/dm-bradgroux/veritas-kanban.git
|
||||
cd veritas-kanban
|
||||
|
||||
# Set up environment variables
|
||||
cp server/.env.example server/.env
|
||||
# Edit server/.env — at minimum, change VERITAS_ADMIN_KEY
|
||||
|
||||
# (Optional) Set up web env if you need a custom API URL
|
||||
# cp web/.env.example web/.env
|
||||
|
||||
# Install
|
||||
pnpm install
|
||||
|
||||
|
|
@ -57,6 +64,8 @@ pnpm dev
|
|||
|
||||
Open http://localhost:3000
|
||||
|
||||
> **Note:** Never commit `.env` files. Use `.env.example` as a template — it contains safe placeholder values and documentation for every variable.
|
||||
|
||||
## Tech Stack
|
||||
|
||||
| Layer | Technology |
|
||||
|
|
|
|||
9
pnpm-lock.yaml
generated
9
pnpm-lock.yaml
generated
|
|
@ -115,6 +115,9 @@ importers:
|
|||
gray-matter:
|
||||
specifier: ^4.0.3
|
||||
version: 4.0.3
|
||||
helmet:
|
||||
specifier: ^8.1.0
|
||||
version: 8.1.0
|
||||
jsonwebtoken:
|
||||
specifier: ^9.0.3
|
||||
version: 9.0.3
|
||||
|
|
@ -2597,6 +2600,10 @@ packages:
|
|||
resolution: {integrity: sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==}
|
||||
engines: {node: '>= 0.4'}
|
||||
|
||||
helmet@8.1.0:
|
||||
resolution: {integrity: sha512-jOiHyAZsmnr8LqoPGmCjYAaiuWwjAPLgY8ZX2XrmHawt99/u1y6RgrZMTeoPfpUbV96HOalYgz1qzkRbw54Pmg==}
|
||||
engines: {node: '>=18.0.0'}
|
||||
|
||||
hermes-estree@0.25.1:
|
||||
resolution: {integrity: sha512-0wUoCcLp+5Ev5pDW2OriHC2MJCbwLwuRx+gAqMTOkGKJJiBCLjtrvy4PWUGn6MIVefecRpzoOZ/UV6iGdOr+Cw==}
|
||||
|
||||
|
|
@ -6516,6 +6523,8 @@ snapshots:
|
|||
dependencies:
|
||||
function-bind: 1.1.2
|
||||
|
||||
helmet@8.1.0: {}
|
||||
|
||||
hermes-estree@0.25.1: {}
|
||||
|
||||
hermes-parser@0.25.1:
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
# Veritas Kanban Server - Environment Variables
|
||||
# Copy to .env and customize
|
||||
# Copy to .env and customize:
|
||||
# cp .env.example .env
|
||||
|
||||
# Server port
|
||||
PORT=3001
|
||||
|
|
@ -26,6 +27,27 @@ VERITAS_ADMIN_KEY=your-secret-admin-key-here
|
|||
# Example:
|
||||
VERITAS_API_KEYS=veritas-agent:vk_agent123:agent,dashboard:vk_dashboard456:read-only
|
||||
|
||||
# ═══════════════════════════════════════════════════════════════════════════════
|
||||
# CORS & NETWORKING
|
||||
# ═══════════════════════════════════════════════════════════════════════════════
|
||||
|
||||
# Allowed CORS origins (comma-separated)
|
||||
CORS_ORIGINS=http://localhost:3000,http://localhost:5173,http://127.0.0.1:3000,http://127.0.0.1:5173
|
||||
|
||||
# ═══════════════════════════════════════════════════════════════════════════════
|
||||
# DATA & STORAGE
|
||||
# ═══════════════════════════════════════════════════════════════════════════════
|
||||
|
||||
# Data directory for tasks, config, logs (default: .veritas-kanban in project root)
|
||||
# VERITAS_DATA_DIR=.veritas-kanban
|
||||
|
||||
# ═══════════════════════════════════════════════════════════════════════════════
|
||||
# INTEGRATION
|
||||
# ═══════════════════════════════════════════════════════════════════════════════
|
||||
|
||||
# Clawdbot gateway URL for AI agent orchestration
|
||||
# CLAWDBOT_GATEWAY=http://127.0.0.1:18789
|
||||
|
||||
# ═══════════════════════════════════════════════════════════════════════════════
|
||||
# ROLE PERMISSIONS
|
||||
# ═══════════════════════════════════════════════════════════════════════════════
|
||||
|
|
|
|||
|
|
@ -1,4 +1,37 @@
|
|||
[
|
||||
{
|
||||
"id": "activity_1769623447737_ioe02qo88",
|
||||
"type": "status_changed",
|
||||
"taskId": "task_20260128_xu0DY5",
|
||||
"taskTitle": "SECURITY: Encrypt security config file or move to env vars",
|
||||
"details": {
|
||||
"from": "todo",
|
||||
"status": "in-progress"
|
||||
},
|
||||
"timestamp": "2026-01-28T18:04:07.737Z"
|
||||
},
|
||||
{
|
||||
"id": "activity_1769623446933_zsztan3h6",
|
||||
"type": "status_changed",
|
||||
"taskId": "task_20260128_VVqEG1",
|
||||
"taskTitle": "SECURITY: Add Content Security Policy (CSP) headers with Helmet",
|
||||
"details": {
|
||||
"from": "todo",
|
||||
"status": "in-progress"
|
||||
},
|
||||
"timestamp": "2026-01-28T18:04:06.933Z"
|
||||
},
|
||||
{
|
||||
"id": "activity_1769623446746_26xrzwcwv",
|
||||
"type": "status_changed",
|
||||
"taskId": "task_20260128_u73mT3",
|
||||
"taskTitle": "SECURITY: Remove .env from git and create .env.example",
|
||||
"details": {
|
||||
"from": "todo",
|
||||
"status": "in-progress"
|
||||
},
|
||||
"timestamp": "2026-01-28T18:04:06.746Z"
|
||||
},
|
||||
{
|
||||
"id": "activity_1769622035872_xdoj9wnyj",
|
||||
"type": "comment_added",
|
||||
|
|
|
|||
|
|
@ -1,4 +1,12 @@
|
|||
[
|
||||
{
|
||||
"id": "status_1769623414045_xz4xmeaz8",
|
||||
"timestamp": "2026-01-28T18:03:34.045Z",
|
||||
"previousStatus": "idle",
|
||||
"newStatus": "sub-agent",
|
||||
"subAgentCount": 0,
|
||||
"durationMs": 2355036
|
||||
},
|
||||
{
|
||||
"id": "status_1769621059009_mq2mkr5m1",
|
||||
"timestamp": "2026-01-28T17:24:19.009Z",
|
||||
|
|
|
|||
|
|
@ -24,6 +24,7 @@
|
|||
"exceljs": "^4.4.0",
|
||||
"express": "^4.21.0",
|
||||
"gray-matter": "^4.0.3",
|
||||
"helmet": "^8.1.0",
|
||||
"jsonwebtoken": "^9.0.3",
|
||||
"mammoth": "^1.11.0",
|
||||
"mime-types": "^3.0.2",
|
||||
|
|
|
|||
|
|
@ -30,6 +30,9 @@ let cachedConfig: SecurityConfig | null = null;
|
|||
let lastLoadTime = 0;
|
||||
const CACHE_TTL_MS = 1000; // Reload every second in dev
|
||||
|
||||
// In-memory JWT secret (generated at runtime if not in env or config)
|
||||
let runtimeJwtSecret: string | null = null;
|
||||
|
||||
/**
|
||||
* Load security config from disk
|
||||
*/
|
||||
|
|
@ -60,6 +63,37 @@ export function getSecurityConfig(): SecurityConfig {
|
|||
return cachedConfig;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get JWT signing secret.
|
||||
* Priority: VERITAS_JWT_SECRET env var > security.json > runtime-generated
|
||||
*
|
||||
* If using env var, the secret is never written to disk.
|
||||
* If falling back to generated, it's stored in memory only (sessions
|
||||
* invalidate on restart unless env var or config file provides persistence).
|
||||
*/
|
||||
export function getJwtSecret(): string {
|
||||
// 1. Environment variable (preferred — never touches disk)
|
||||
const envSecret = process.env.VERITAS_JWT_SECRET;
|
||||
if (envSecret) {
|
||||
return envSecret;
|
||||
}
|
||||
|
||||
// 2. security.json (legacy / fallback for existing installs)
|
||||
const config = getSecurityConfig();
|
||||
if (config.jwtSecret) {
|
||||
return config.jwtSecret;
|
||||
}
|
||||
|
||||
// 3. Runtime-generated (ephemeral — sessions won't survive restart)
|
||||
if (!runtimeJwtSecret) {
|
||||
runtimeJwtSecret = crypto.randomBytes(64).toString('hex');
|
||||
console.warn(
|
||||
'JWT secret generated at runtime. Set VERITAS_JWT_SECRET env var for persistence across restarts.'
|
||||
);
|
||||
}
|
||||
return runtimeJwtSecret;
|
||||
}
|
||||
|
||||
/**
|
||||
* Save security config to disk
|
||||
*/
|
||||
|
|
@ -126,6 +160,7 @@ export function resetSecurityConfig(): void {
|
|||
authEnabled: false,
|
||||
};
|
||||
saveSecurityConfig(newConfig);
|
||||
runtimeJwtSecret = null;
|
||||
console.log('Security config reset. Next load will show setup screen.');
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
import 'dotenv/config';
|
||||
import express from 'express';
|
||||
import helmet from 'helmet';
|
||||
import cors from 'cors';
|
||||
import cookieParser from 'cookie-parser';
|
||||
import { WebSocketServer, WebSocket } from 'ws';
|
||||
|
|
@ -44,6 +45,58 @@ import type { AgentOutput } from './services/clawdbot-agent-service.js';
|
|||
const app = express();
|
||||
const PORT = process.env.PORT || 3001;
|
||||
|
||||
// ============================================
|
||||
// Security: HTTP Headers (Helmet)
|
||||
// ============================================
|
||||
// Helmet sets various HTTP headers to help protect the app.
|
||||
// Content-Security-Policy (CSP) restricts which resources the browser
|
||||
// is allowed to load, mitigating XSS and data-injection attacks.
|
||||
//
|
||||
// CSP Directives:
|
||||
// defaultSrc - Fallback for all resource types: only same-origin
|
||||
// scriptSrc - Scripts: same-origin + inline (needed for Vite HMR in dev)
|
||||
// styleSrc - Styles: same-origin + inline (Tailwind/JSX inline styles)
|
||||
// connectSrc - XHR/fetch/WebSocket: same-origin + ws://localhost for dev WS
|
||||
// imgSrc - Images: same-origin + data: URIs (inline SVGs, base64 images)
|
||||
// fontSrc - Fonts: same-origin
|
||||
// objectSrc - Plugins (Flash, etc.): blocked entirely
|
||||
// frameSrc - Iframes: blocked entirely
|
||||
// baseUri - <base> tag: only same-origin
|
||||
// formAction - Form submissions: only same-origin
|
||||
// upgradeInsecureRequests - Auto-upgrade HTTP → HTTPS in production
|
||||
//
|
||||
// In development, connectSrc includes ws://localhost:* for WebSocket hot-reload.
|
||||
// In production, tighten scriptSrc (remove 'unsafe-inline') and use nonces.
|
||||
const isDev = process.env.NODE_ENV !== 'production';
|
||||
|
||||
app.use(
|
||||
helmet({
|
||||
contentSecurityPolicy: {
|
||||
directives: {
|
||||
defaultSrc: ["'self'"],
|
||||
scriptSrc: ["'self'", ...(isDev ? ["'unsafe-inline'", "'unsafe-eval'"] : [])],
|
||||
styleSrc: ["'self'", "'unsafe-inline'"],
|
||||
connectSrc: [
|
||||
"'self'",
|
||||
...(isDev
|
||||
? ['ws://localhost:*', 'ws://127.0.0.1:*', 'http://localhost:*', 'http://127.0.0.1:*']
|
||||
: []),
|
||||
],
|
||||
imgSrc: ["'self'", 'data:', 'blob:'],
|
||||
fontSrc: ["'self'"],
|
||||
objectSrc: ["'none'"],
|
||||
frameSrc: ["'none'"],
|
||||
baseUri: ["'self'"],
|
||||
formAction: ["'self'"],
|
||||
...(isDev ? {} : { upgradeInsecureRequests: [] }),
|
||||
},
|
||||
},
|
||||
// Cross-Origin-Embedder-Policy can break loading of cross-origin resources;
|
||||
// disable it for now since we serve an API, not embedded content.
|
||||
crossOriginEmbedderPolicy: false,
|
||||
})
|
||||
);
|
||||
|
||||
// ============================================
|
||||
// Security: CORS Configuration
|
||||
// ============================================
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@ import { Request, Response, NextFunction } from 'express';
|
|||
import { WebSocket } from 'ws';
|
||||
import { IncomingMessage } from 'http';
|
||||
import jwt from 'jsonwebtoken';
|
||||
import { getSecurityConfig } from '../config/security.js';
|
||||
import { getSecurityConfig, getJwtSecret } from '../config/security.js';
|
||||
|
||||
// === Types ===
|
||||
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ import crypto from 'crypto';
|
|||
import { asyncHandler } from '../middleware/async-handler.js';
|
||||
import {
|
||||
getSecurityConfig,
|
||||
getJwtSecret,
|
||||
saveSecurityConfig,
|
||||
SecurityConfig,
|
||||
generateRecoveryKey,
|
||||
|
|
@ -18,17 +19,6 @@ const SALT_ROUNDS = 12;
|
|||
const JWT_EXPIRY_DEFAULT = '24h';
|
||||
const JWT_EXPIRY_REMEMBER = '30d';
|
||||
|
||||
// Get or generate JWT secret
|
||||
function getJwtSecret(): string {
|
||||
const config = getSecurityConfig();
|
||||
if (!config.jwtSecret) {
|
||||
const secret = crypto.randomBytes(64).toString('hex');
|
||||
saveSecurityConfig({ ...config, jwtSecret: secret });
|
||||
return secret;
|
||||
}
|
||||
return config.jwtSecret;
|
||||
}
|
||||
|
||||
// Rate limiting for login attempts (in-memory, resets on restart)
|
||||
const loginAttempts = new Map<string, { count: number; lastAttempt: number }>();
|
||||
const MAX_ATTEMPTS = 5;
|
||||
|
|
@ -144,18 +134,22 @@ router.post('/setup', asyncHandler(async (req: Request, res: Response) => {
|
|||
const recoveryKey = generateRecoveryKey();
|
||||
const recoveryKeyHash = await hashRecoveryKey(recoveryKey);
|
||||
|
||||
// Generate JWT secret
|
||||
const jwtSecret = crypto.randomBytes(64).toString('hex');
|
||||
|
||||
// Save config
|
||||
saveSecurityConfig({
|
||||
// Build config to save — only persist jwtSecret to file if env var is NOT set
|
||||
const updatedConfig: SecurityConfig = {
|
||||
...config,
|
||||
passwordHash,
|
||||
recoveryKeyHash,
|
||||
jwtSecret,
|
||||
authEnabled: true,
|
||||
setupCompletedAt: new Date().toISOString(),
|
||||
});
|
||||
};
|
||||
|
||||
// If no env var, generate and persist a JWT secret to the config file
|
||||
if (!process.env.VERITAS_JWT_SECRET) {
|
||||
updatedConfig.jwtSecret = crypto.randomBytes(64).toString('hex');
|
||||
}
|
||||
|
||||
// Save config
|
||||
saveSecurityConfig(updatedConfig);
|
||||
|
||||
// Return recovery key (only time it's shown in plaintext)
|
||||
res.json({
|
||||
|
|
@ -314,17 +308,22 @@ router.post('/recover', asyncHandler(async (req: Request, res: Response) => {
|
|||
const newRecoveryKey = generateRecoveryKey();
|
||||
const newRecoveryKeyHash = await hashRecoveryKey(newRecoveryKey);
|
||||
|
||||
// Generate new JWT secret (invalidates all existing sessions)
|
||||
const jwtSecret = crypto.randomBytes(64).toString('hex');
|
||||
|
||||
// Save config
|
||||
saveSecurityConfig({
|
||||
// Build config — rotate jwtSecret in file only if env var is NOT set
|
||||
const updatedConfig: SecurityConfig = {
|
||||
...config,
|
||||
passwordHash,
|
||||
recoveryKeyHash: newRecoveryKeyHash,
|
||||
jwtSecret,
|
||||
lastPasswordChange: new Date().toISOString(),
|
||||
});
|
||||
};
|
||||
|
||||
// Rotate file-based secret if no env var (invalidates all existing sessions)
|
||||
if (!process.env.VERITAS_JWT_SECRET) {
|
||||
updatedConfig.jwtSecret = crypto.randomBytes(64).toString('hex');
|
||||
}
|
||||
// Note: if using env var, session invalidation requires changing the env var
|
||||
|
||||
// Save config
|
||||
saveSecurityConfig(updatedConfig);
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
|
|
|
|||
7
web/.env.example
Normal file
7
web/.env.example
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
# Veritas Kanban Web - Environment Variables
|
||||
# Copy this file to .env and update the values:
|
||||
# cp .env.example .env
|
||||
|
||||
# API base URL (default: /api — uses Vite proxy in dev)
|
||||
# Set this if running the server on a different host/port
|
||||
# VITE_API_URL=http://localhost:3001/api
|
||||
Loading…
Add table
Reference in a new issue