Skip to content
Merged
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
56 changes: 56 additions & 0 deletions .env.production.example
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
# CodeFRAME Production Environment Configuration
# Copy this file to .env.production and add your actual values.
# ecosystem.production.config.js reads these values.

# Anthropic API Key (required for Lead Agent functionality)
ANTHROPIC_API_KEY=your-anthropic-api-key-here

# Authentication secret (REQUIRED)
# The backend hard-fails on startup if this is unset in production/hosted mode
# (see issue #643). Generate with:
# openssl rand -hex 32
AUTH_SECRET=your-secure-random-secret-here

# Database Path (production database location)
DATABASE_PATH=/home/frankbria/projects/codeframe/.codeframe/state.db

# Server Configuration
# Both app processes bind loopback and sit behind the Caddy reverse proxy that
# terminates TLS (#747). HOST pins the backend to 127.0.0.1; the frontend is
# pinned in ecosystem.production.config.js. Never bind 0.0.0.0 on a shared VPS.
BACKEND_PORT=8000
FRONTEND_PORT=3000
HOST=127.0.0.1

# PM2 app names (optional — override the ecosystem defaults, #727)
# PM2_BACKEND_NAME=codeframe-production-backend
# PM2_FRONTEND_NAME=codeframe-production-frontend

# Public origins (NEXT_PUBLIC_* variables are exposed to the browser). These are
# your domain served by Caddy over TLS — the browser only ever speaks https/wss.
# Caddy path-routes /api, /auth and /ws to the backend, so this is same-origin.
# See deploy/README.md and deploy/Caddyfile.example.
NEXT_PUBLIC_API_URL=https://your-domain

# WebSocket URL for real-time updates — wss (TLS) via the reverse proxy.
NEXT_PUBLIC_WS_URL=wss://your-domain

# CORS Configuration
# With the reverse proxy the browser hits a single https origin (same-origin),
# so this is just that domain.
CORS_ALLOWED_ORIGINS=https://your-domain

# Trusted proxy for rate limiting — Caddy connects over loopback, so trust
# 127.0.0.1 to read the real client IP from X-Forwarded-For. Without this the
# rate limiter keys every request to the proxy IP, collapsing per-client and
# auth brute-force limits into one shared bucket (#747).
RATE_LIMIT_TRUSTED_PROXIES=127.0.0.1

# Environment
# NODE_ENV must be 'production' for Next.js to work correctly.
NODE_ENV=production
CODEFRAME_ENV=production
PYTHON_ENV=production

# Logging
LOG_LEVEL=INFO
40 changes: 20 additions & 20 deletions .env.staging.example
Original file line number Diff line number Diff line change
Expand Up @@ -14,32 +14,32 @@ AUTH_SECRET=your-secure-random-secret-here
DATABASE_PATH=/home/frankbria/projects/codeframe/staging/.codeframe/state.db

# Server Configuration
# Both app processes bind loopback and sit behind the Caddy reverse proxy that
# terminates TLS (#747). HOST pins the backend to 127.0.0.1; the frontend is
# pinned in ecosystem.staging.config.js. Never bind 0.0.0.0 on a shared VPS.
BACKEND_PORT=14200
FRONTEND_PORT=14100
HOST=0.0.0.0
HOST=127.0.0.1

# Frontend API URL (NEXT_PUBLIC_* variables are exposed to the browser)
# This tells the Next.js frontend where the backend API is located
NEXT_PUBLIC_API_URL=http://localhost:14200
# Public origins (NEXT_PUBLIC_* variables are exposed to the browser). These are
# your domain served by Caddy over TLS — the browser only ever speaks https/wss.
# Caddy path-routes /api, /auth and /ws to the backend, so this is same-origin.
# See deploy/README.md and deploy/Caddyfile.example.
NEXT_PUBLIC_API_URL=https://your-domain

# WebSocket URL for real-time updates
# Use ws:// for HTTP or wss:// for HTTPS
# Development: ws://localhost:8080/ws
# Staging: ws://your-hostname/ws (if nginx proxies WebSocket)
# Production: wss://your-domain/ws
NEXT_PUBLIC_WS_URL=ws://localhost:14200/ws
# WebSocket URL for real-time updates — wss (TLS) via the reverse proxy.
NEXT_PUBLIC_WS_URL=wss://your-domain

# CORS Configuration
# Comma-separated list of allowed origins for CORS
# IMPORTANT: Add your server's hostname/IP if accessing via network
# Development: http://localhost:3000,http://localhost:5173
# Staging (localhost): http://localhost:14100,http://0.0.0.0:14100
# Staging (network): http://your-server-hostname:14100
# Production: https://yourdomain.com
#
# Example for network access via hostname:
# CORS_ALLOWED_ORIGINS=http://localhost:14100,http://frankbria-inspiron-7586:14100,http://codeframe.home.frankbria.net
CORS_ALLOWED_ORIGINS=http://localhost:14100,http://0.0.0.0:14100
# Comma-separated list of allowed origins. With the reverse proxy the browser
# hits a single https origin (same-origin), so this is just that domain.
CORS_ALLOWED_ORIGINS=https://your-domain

# Trusted proxy for rate limiting — Caddy connects over loopback, so trust
# 127.0.0.1 to read the real client IP from X-Forwarded-For. Without this the
# rate limiter keys every request to the proxy IP, collapsing per-client and
# auth brute-force limits into one shared bucket (#747).
RATE_LIMIT_TRUSTED_PROXIES=127.0.0.1

# Environment
# NODE_ENV should be 'production' for Next.js to work correctly
Expand Down
39 changes: 39 additions & 0 deletions deploy/Caddyfile.example
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
# CodeFRAME reverse proxy — terminates TLS in front of the app processes (#747).
#
# The backend (127.0.0.1:14200) and frontend (127.0.0.1:14100) bind loopback
# only; this proxy is the sole public listener. Caddy automatically provisions
# and renews Let's Encrypt certificates for a real domain, so JWT bearer tokens
# and API keys never travel in cleartext.
#
# Setup on the VPS:
# 1. Point your domain's A/AAAA record at this server.
# 2. Replace codeframe.example.com below with your domain.
# 3. sudo cp deploy/Caddyfile.example /etc/caddy/Caddyfile
# 4. sudo systemctl reload caddy
#
# No public domain (IP-only / internal host)? Replace the site address with the
# IP and add `tls internal` to serve with Caddy's local CA:
#
# 203.0.113.10 {
# tls internal
# ...
# }
#
# Note: browsers reject Caddy's local CA (NET::ERR_CERT_AUTHORITY_INVALID) until
# you install its root cert on the client (`caddy trust`) or accept the warning.
#
# Ports below match the staging deploy (14100 frontend / 14200 backend). For the
# production ecosystem, set them to your FRONTEND_PORT / BACKEND_PORT
# (defaults 3000 / 8000).

codeframe.example.com {
encode zstd gzip

# Backend: REST API, auth, WebSockets, health, and API docs. Caddy proxies
# WebSocket upgrades (/ws/*) transparently — no extra config needed.
@backend path /api/* /auth/* /ws/* /health /docs /redoc /openapi.json
reverse_proxy @backend 127.0.0.1:14200

# Everything else → Next.js frontend.
reverse_proxy 127.0.0.1:14100
}
56 changes: 56 additions & 0 deletions deploy/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
# Deployment: TLS reverse proxy

CodeFRAME runs two processes behind a **TLS-terminating reverse proxy** (issue #747):

| Process | Binds | Public? |
|----------|------------------|---------|
| Backend | `127.0.0.1:14200` | no — loopback only |
| Frontend | `127.0.0.1:14100` | no — loopback only |
| Caddy | `:80`, `:443` | **yes** — the only public listener |

Because the app processes bind loopback, JWT bearer tokens and API keys only
ever travel over HTTPS/WSS between the browser and Caddy. Plaintext
`http://`/`ws://` is confined to the loopback hop, which never leaves the host.

## Setup

`scripts/remote-setup.sh` installs Caddy and points you here. Manually:

1. Point your domain's DNS `A`/`AAAA` record at the server.
2. Copy the config and set your domain:
```bash
sudo cp deploy/Caddyfile.example /etc/caddy/Caddyfile
sudo $EDITOR /etc/caddy/Caddyfile # replace codeframe.example.com
sudo systemctl reload caddy
```
Caddy provisions and renews the Let's Encrypt certificate automatically.
3. Set the public origins in `.env.staging` / `.env.production`:
```
NEXT_PUBLIC_API_URL=https://your-domain
NEXT_PUBLIC_WS_URL=wss://your-domain
CORS_ALLOWED_ORIGINS=https://your-domain
HOST=127.0.0.1
```
4. Firewall: allow `80`/`443` only. The app ports (`14100`/`14200`) must **not**
be reachable from the network — Caddy reaches them over loopback.

## Routing

Caddy path-routes a single public origin, so browser traffic is same-origin and
CORS pre-flight never fires. Keep `CORS_ALLOWED_ORIGINS` set to that domain
anyway — the backend's CORS middleware still validates it, and it's the fallback
if you later split the API onto a separate subdomain.

- `/api/*`, `/auth/*`, `/ws/*`, `/health`, `/docs`, `/redoc`, `/openapi.json`
→ backend `127.0.0.1:14200`
- everything else → Next.js frontend `127.0.0.1:14100`

WebSocket upgrades are handled transparently by Caddy's `reverse_proxy`.

## No public domain?

For an IP-only or internal host, use the IP as the site address and add
`tls internal` (Caddy's local CA) — see the commented example in
`Caddyfile.example`. Browsers reject the local CA
(`NET::ERR_CERT_AUTHORITY_INVALID`) until its root cert is trusted on the client
(`caddy trust`) or the warning is accepted — this is expected, not a broken deploy.
10 changes: 8 additions & 2 deletions ecosystem.production.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -17,13 +17,17 @@ const FRONTEND_NAME =
process.env.PM2_FRONTEND_NAME || envConfig.PM2_FRONTEND_NAME || 'codeframe-production-frontend';
const BACKEND_PORT = process.env.BACKEND_PORT || envConfig.BACKEND_PORT || '8000';
const FRONTEND_PORT = process.env.FRONTEND_PORT || envConfig.FRONTEND_PORT || '3000';
// Bind loopback by default — the Caddy reverse proxy is the sole public listener
// (#747). Defaults to 127.0.0.1 so a stale HOST in .env.production can't expose
// the backend; override only for a proxy on a separate host.
const HOST = process.env.HOST || envConfig.HOST || '127.0.0.1';

module.exports = {
apps: [
{
name: BACKEND_NAME,
script: path.join(PROJECT_ROOT, '.venv/bin/python'),
args: `-m codeframe.ui.server --port ${BACKEND_PORT}`,
args: `-m codeframe.ui.server --host ${HOST} --port ${BACKEND_PORT}`,
cwd: PROJECT_ROOT,
env: {
...envConfig,
Expand All @@ -42,7 +46,9 @@ module.exports = {
{
name: FRONTEND_NAME,
script: path.join(PROJECT_ROOT, 'web-ui/node_modules/.bin/next'),
args: `start -H 0.0.0.0 -p ${FRONTEND_PORT}`,
// Bind loopback only — the Caddy reverse proxy terminates TLS and is the
// sole public listener (issue #747).
args: `start -H 127.0.0.1 -p ${FRONTEND_PORT}`,
cwd: path.join(PROJECT_ROOT, 'web-ui'),
env: {
...envConfig
Expand Down
9 changes: 7 additions & 2 deletions ecosystem.staging.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,10 @@ module.exports = {
{
name: 'codeframe-staging-backend',
script: path.join(PROJECT_ROOT, '.venv/bin/python'),
args: '-m codeframe.ui.server --port 14200',
// Bind loopback explicitly — do not rely on HOST in .env.staging, so an
// existing deploy with a stale HOST=0.0.0.0 can't leave the backend
// exposed behind the TLS proxy (issue #747).
args: '-m codeframe.ui.server --host 127.0.0.1 --port 14200',
cwd: PROJECT_ROOT,
env: {
...envConfig,
Expand All @@ -31,7 +34,9 @@ module.exports = {
{
name: 'codeframe-staging-frontend',
script: path.join(PROJECT_ROOT, 'web-ui/node_modules/.bin/next'),
args: 'start -H 0.0.0.0 -p 14100',
// Bind loopback only — the Caddy reverse proxy terminates TLS and is the
// sole public listener (issue #747).
args: 'start -H 127.0.0.1 -p 14100',
cwd: path.join(PROJECT_ROOT, 'web-ui'),
env: {
...envConfig
Expand Down
86 changes: 57 additions & 29 deletions scripts/remote-setup.sh
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,27 @@ else
echo -e "${GREEN}✓ lsof already installed${NC}"
fi

# Install Caddy — the TLS-terminating reverse proxy that fronts the app (#747).
# The app processes bind 127.0.0.1; Caddy is the only public listener and
# auto-provisions/renews Let's Encrypt certificates.
if ! command -v caddy &> /dev/null; then
echo "Installing Caddy (reverse proxy for TLS termination)..."
sudo apt install -y debian-keyring debian-archive-keyring apt-transport-https curl
curl -1sLf 'https://dl.cloudsmith.io/public/caddy/stable/gpg.key' \
| sudo gpg --dearmor -o /usr/share/keyrings/caddy-stable-archive-keyring.gpg
curl -1sLf 'https://dl.cloudsmith.io/public/caddy/stable/debian.deb.txt' \
| sudo tee /etc/apt/sources.list.d/caddy-stable.list
sudo apt update
sudo apt install -y caddy
echo -e "${GREEN}✓ Caddy installed${NC}"
else
echo -e "${GREEN}✓ Caddy already installed${NC}"
fi

# Verify Caddy installation
CADDY_VERSION=$(caddy version 2>/dev/null || echo "not found")
echo " Caddy version: $CADDY_VERSION"

# ============================================================================
# PHASE 2: PROJECT DIRECTORY SETUP
# ============================================================================
Expand Down Expand Up @@ -195,34 +216,35 @@ echo ""
echo -e "${BLUE}[4/5] Firewall Configuration${NC}"
echo ""

# Check if ufw is active
# Only 80/443 (Caddy) are exposed. The app ports 14100/14200 stay loopback-only
# — Caddy reaches them over 127.0.0.1, so they must NOT be opened to the network
# (#747). If a legacy rule exposed them, deny it.
if sudo ufw status | grep -q "Status: active"; then
echo "UFW firewall is active."

# Check if ports are already allowed
if ! sudo ufw status | grep -q "14100/tcp"; then
read -p "Allow port 14100 (frontend) through firewall? (Y/n): " -n 1 -r
echo
if [[ ! $REPLY =~ ^[Nn]$ ]]; then
sudo ufw allow 14100/tcp comment 'CodeFRAME Frontend'
echo -e "${GREEN}✓ Port 14100 allowed${NC}"
for port in 80 443; do
if ! sudo ufw status | grep -q "${port}/tcp"; then
read -p "Allow port ${port} (Caddy reverse proxy) through firewall? (Y/n): " -n 1 -r
echo
if [[ ! $REPLY =~ ^[Nn]$ ]]; then
sudo ufw allow ${port}/tcp comment 'CodeFRAME Caddy proxy'
echo -e "${GREEN}✓ Port ${port} allowed${NC}"
fi
else
echo -e "${GREEN}✓ Port ${port} already allowed${NC}"
fi
else
echo -e "${GREEN}✓ Port 14100 already allowed${NC}"
fi
done

if ! sudo ufw status | grep -q "14200/tcp"; then
read -p "Allow port 14200 (backend) through firewall? (Y/n): " -n 1 -r
echo
if [[ ! $REPLY =~ ^[Nn]$ ]]; then
sudo ufw allow 14200/tcp comment 'CodeFRAME Backend API'
echo -e "${GREEN}✓ Port 14200 allowed${NC}"
# App ports must never be public — Caddy proxies them over loopback.
for port in 14100 14200; do
if sudo ufw status | grep -q "${port}/tcp.*ALLOW"; then
echo -e "${YELLOW}⚠ Port ${port} is exposed — denying (must be loopback-only)${NC}"
sudo ufw delete allow ${port}/tcp 2>/dev/null || true
fi
else
echo -e "${GREEN}✓ Port 14200 already allowed${NC}"
fi
done
else
echo "UFW firewall is not active. Skipping firewall configuration."
echo "UFW firewall is not active."
echo -e "${YELLOW}⚠ Expose only 80/443. The app ports 14100/14200 must stay loopback-only.${NC}"
fi

# ============================================================================
Expand Down Expand Up @@ -252,22 +274,28 @@ echo ""

echo -e "${BLUE}Next Steps:${NC}"
echo ""
echo "1. Verify your .env.staging configuration:"
echo " cat .env.staging | grep ANTHROPIC_API_KEY"
echo "1. Verify your .env.staging configuration (loopback host + https origins):"
echo " grep -E 'ANTHROPIC_API_KEY|HOST|NEXT_PUBLIC_' .env.staging"
echo ""
echo "2. Configure the Caddy reverse proxy (TLS termination):"
echo " sudo cp deploy/Caddyfile.example /etc/caddy/Caddyfile"
echo " sudo \$EDITOR /etc/caddy/Caddyfile # set your domain"
echo " sudo systemctl reload caddy"
echo " (See deploy/README.md for details.)"
echo ""
echo "2. Run the deployment script:"
echo "3. Run the deployment script:"
echo " cd ~/projects/codeframe"
echo " ./scripts/deploy-staging.sh"
echo ""
echo "3. After deployment, access the dashboard:"
echo " Frontend: http://YOUR_STAGING_SERVER:14100"
echo " Backend: http://YOUR_STAGING_SERVER:14200"
echo "4. After deployment, access the dashboard over TLS via Caddy:"
echo " https://your-domain"
echo " (The app ports 14100/14200 are loopback-only — not public.)"
echo ""
echo "4. (Optional) Enable auto-start on boot:"
echo "5. (Optional) Enable auto-start on boot:"
echo " pm2 save"
echo " pm2 startup"
echo ""

echo -e "${BLUE}Documentation:${NC}"
echo " See docs/REMOTE_STAGING_DEPLOYMENT.md for full guide"
echo " See deploy/README.md for the TLS reverse-proxy setup guide"
echo ""
Loading
Loading