chore: git cache cleanup
This commit is contained in:
253
.docker/README.md
Normal file
253
.docker/README.md
Normal file
@@ -0,0 +1,253 @@
|
||||
# Docker Deployment Guide
|
||||
|
||||
Charon is designed for Docker-first deployment, making it easy for home users to run Caddy without learning Caddyfile syntax.
|
||||
|
||||
## Directory Structure
|
||||
|
||||
```text
|
||||
.docker/
|
||||
├── compose/ # Docker Compose files
|
||||
│ ├── docker-compose.yml # Main production compose
|
||||
│ ├── docker-compose.dev.yml # Development overrides
|
||||
│ ├── docker-compose.local.yml # Local development
|
||||
│ ├── docker-compose.remote.yml # Remote deployment
|
||||
│ └── docker-compose.override.yml # Personal overrides (gitignored)
|
||||
├── docker-entrypoint.sh # Container entrypoint script
|
||||
└── README.md # This file
|
||||
```
|
||||
|
||||
## Quick Start
|
||||
|
||||
```bash
|
||||
# Clone the repository
|
||||
git clone https://github.com/Wikid82/charon.git
|
||||
cd charon
|
||||
|
||||
# Start the stack (using new location)
|
||||
docker compose -f .docker/compose/docker-compose.yml up -d
|
||||
|
||||
# Access the UI
|
||||
open http://localhost:8080
|
||||
```
|
||||
|
||||
## Usage
|
||||
|
||||
When running docker-compose commands, specify the compose file location:
|
||||
|
||||
```bash
|
||||
# Production
|
||||
docker compose -f .docker/compose/docker-compose.yml up -d
|
||||
|
||||
# Development
|
||||
docker compose -f .docker/compose/docker-compose.yml -f .docker/compose/docker-compose.dev.yml up -d
|
||||
|
||||
# Local development
|
||||
docker compose -f .docker/compose/docker-compose.local.yml up -d
|
||||
|
||||
# With personal overrides
|
||||
docker compose -f .docker/compose/docker-compose.yml -f .docker/compose/docker-compose.override.yml up -d
|
||||
```
|
||||
|
||||
## Architecture
|
||||
|
||||
Charon runs as a **single container** that includes:
|
||||
|
||||
1. **Caddy Server**: The reverse proxy engine (ports 80/443).
|
||||
2. **Charon Backend**: The Go API that manages Caddy via its API (binary: `charon`, `cpmp` symlink preserved).
|
||||
3. **Charon Frontend**: The React web interface (port 8080).
|
||||
|
||||
This unified architecture simplifies deployment, updates, and data management.
|
||||
|
||||
```text
|
||||
┌──────────────────────────────────────────┐
|
||||
│ Container (charon / cpmp) │
|
||||
│ │
|
||||
│ ┌──────────┐ API ┌──────────────┐ │
|
||||
│ │ Caddy │◄──:2019──┤ Charon App │ │
|
||||
│ │ (Proxy) │ │ (Manager) │ │
|
||||
│ └────┬─────┘ └──────┬───────┘ │
|
||||
│ │ │ │
|
||||
└───────┼───────────────────────┼──────────┘
|
||||
│ :80, :443 │ :8080
|
||||
▼ ▼
|
||||
Internet Web UI
|
||||
```
|
||||
|
||||
## Configuration
|
||||
|
||||
### Volumes
|
||||
|
||||
Persist your data by mounting these volumes:
|
||||
|
||||
| Host Path | Container Path | Description |
|
||||
|-----------|----------------|-------------|
|
||||
| `./data` | `/app/data` | **Critical**. Stores the SQLite database (default `charon.db`, `cpm.db` fallback) and application logs. |
|
||||
| `./caddy_data` | `/data` | **Critical**. Stores Caddy's SSL certificates and keys. |
|
||||
| `./caddy_config` | `/config` | Stores Caddy's autosave configuration. |
|
||||
|
||||
### Environment Variables
|
||||
|
||||
Configure the application via `docker-compose.yml`:
|
||||
|
||||
| Variable | Default | Description |
|
||||
|----------|---------|-------------|
|
||||
| `CHARON_ENV` | `production` | Set to `development` for verbose logging (`CPM_ENV` supported for backward compatibility). |
|
||||
| `CHARON_HTTP_PORT` | `8080` | Port for the Web UI (`CPM_HTTP_PORT` supported for backward compatibility). |
|
||||
| `CHARON_DB_PATH` | `/app/data/charon.db` | Path to the SQLite database (`CPM_DB_PATH` supported for backward compatibility). |
|
||||
| `CHARON_CADDY_ADMIN_API` | `http://localhost:2019` | Internal URL for Caddy API (`CPM_CADDY_ADMIN_API` supported for backward compatibility). Must resolve to an internal allowlisted host on port `2019`. |
|
||||
| `CHARON_CADDY_CONFIG_ROOT` | `/config` | Path to Caddy autosave configuration directory. |
|
||||
| `CHARON_CADDY_LOG_DIR` | `/var/log/caddy` | Directory for Caddy access logs. |
|
||||
| `CHARON_CROWDSEC_LOG_DIR` | `/var/log/crowdsec` | Directory for CrowdSec logs. |
|
||||
| `CHARON_PLUGINS_DIR` | `/app/plugins` | Directory for DNS provider plugins. |
|
||||
| `CHARON_SINGLE_CONTAINER_MODE` | `true` | Enables permission repair endpoints for single-container deployments. |
|
||||
|
||||
## NAS Deployment Guides
|
||||
|
||||
### Synology (Container Manager / Docker)
|
||||
|
||||
1. **Prepare Folders**: Create a folder `docker/charon` (or `docker/cpmp` for backward compatibility) and subfolders `data`, `caddy_data`, and `caddy_config`.
|
||||
2. **Download Image**: Search for `ghcr.io/wikid82/charon` in the Registry and download the `latest` tag.
|
||||
3. **Launch Container**:
|
||||
- **Network**: Use `Host` mode (recommended for Caddy to see real client IPs) OR bridge mode mapping ports `80:80`, `443:443`, and `8080:8080`.
|
||||
- **Volume Settings**:
|
||||
- `/docker/charon/data` -> `/app/data` (or `/docker/cpmp/data` -> `/app/data` for backward compatibility)
|
||||
- `/docker/charon/caddy_data` -> `/data` (or `/docker/cpmp/caddy_data` -> `/data` for backward compatibility)
|
||||
- `/docker/charon/caddy_config` -> `/config` (or `/docker/cpmp/caddy_config` -> `/config` for backward compatibility)
|
||||
- **Environment**: Add `CHARON_ENV=production` (or `CPM_ENV=production` for backward compatibility).
|
||||
4. **Finish**: Start the container and access `http://YOUR_NAS_IP:8080`.
|
||||
|
||||
### Unraid
|
||||
|
||||
1. **Community Apps**: (Coming Soon) Search for "charon".
|
||||
2. **Manual Install**:
|
||||
- Click **Add Container**.
|
||||
- **Name**: Charon
|
||||
- **Repository**: `ghcr.io/wikid82/charon:latest`
|
||||
- **Network Type**: Bridge
|
||||
- **WebUI**: `http://[IP]:[PORT:8080]`
|
||||
- **Port mappings**:
|
||||
- Container Port: `80` -> Host Port: `80`
|
||||
- Container Port: `443` -> Host Port: `443`
|
||||
- Container Port: `8080` -> Host Port: `8080`
|
||||
- **Paths**:
|
||||
- `/mnt/user/appdata/charon/data` -> `/app/data` (or `/mnt/user/appdata/cpmp/data` -> `/app/data` for backward compatibility)
|
||||
- `/mnt/user/appdata/charon/caddy_data` -> `/data` (or `/mnt/user/appdata/cpmp/caddy_data` -> `/data` for backward compatibility)
|
||||
- `/mnt/user/appdata/charon/caddy_config` -> `/config` (or `/mnt/user/appdata/cpmp/caddy_config` -> `/config` for backward compatibility)
|
||||
3. **Apply**: Click Done to pull and start.
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### App can't reach Caddy
|
||||
|
||||
**Symptom**: "Caddy unreachable" errors in logs
|
||||
|
||||
**Solution**: Since both run in the same container, this usually means Caddy failed to start. Check logs:
|
||||
|
||||
```bash
|
||||
docker compose -f .docker/compose/docker-compose.yml logs app
|
||||
```
|
||||
|
||||
### Certificates not working
|
||||
|
||||
**Symptom**: HTTP works but HTTPS fails
|
||||
|
||||
**Check**:
|
||||
|
||||
1. Port 80/443 are accessible from the internet
|
||||
2. DNS points to your server
|
||||
3. Caddy logs: `docker compose -f .docker/compose/docker-compose.yml logs app | grep -i acme`
|
||||
|
||||
### Config changes not applied
|
||||
|
||||
**Symptom**: Changes in UI don't affect routing
|
||||
|
||||
**Debug**:
|
||||
|
||||
```bash
|
||||
# View current Caddy config
|
||||
curl http://localhost:2019/config/ | jq
|
||||
|
||||
# Check Charon logs
|
||||
docker compose -f .docker/compose/docker-compose.yml logs app
|
||||
|
||||
# Manual config reload
|
||||
curl -X POST http://localhost:8080/api/v1/caddy/reload
|
||||
```
|
||||
|
||||
## Updating
|
||||
|
||||
Pull the latest images and restart:
|
||||
|
||||
```bash
|
||||
docker compose -f .docker/compose/docker-compose.yml pull
|
||||
docker compose -f .docker/compose/docker-compose.yml up -d
|
||||
```
|
||||
|
||||
For specific versions:
|
||||
|
||||
```bash
|
||||
# Edit docker-compose.yml to pin version
|
||||
image: ghcr.io/wikid82/charon:v1.0.0
|
||||
|
||||
docker compose -f .docker/compose/docker-compose.yml up -d
|
||||
```
|
||||
|
||||
## Building from Source
|
||||
|
||||
```bash
|
||||
# Build multi-arch images
|
||||
docker buildx build --platform linux/amd64,linux/arm64 -t charon:local .
|
||||
|
||||
# Or use Make
|
||||
make docker-build
|
||||
```
|
||||
|
||||
## Security Considerations
|
||||
|
||||
1. **Caddy admin API**: Keep port 2019 internal (not exposed in production compose)
|
||||
2. **Management UI**: Add authentication (Issue #7) before exposing to internet
|
||||
3. **Certificates**: Caddy stores private keys in `caddy_data` - protect this volume
|
||||
4. **Database**: SQLite file contains all config - backup regularly
|
||||
|
||||
## Integration with Existing Caddy
|
||||
|
||||
If you already have Caddy running, you can point Charon to it:
|
||||
|
||||
```yaml
|
||||
environment:
|
||||
- CPM_CADDY_ADMIN_API=http://your-caddy-host:2019
|
||||
```
|
||||
|
||||
If using a non-localhost internal hostname, add it to `CHARON_SSRF_INTERNAL_HOST_ALLOWLIST`.
|
||||
|
||||
**Warning**: Charon will replace Caddy's entire configuration. Backup first!
|
||||
|
||||
## Performance Tuning
|
||||
|
||||
For high-traffic deployments:
|
||||
|
||||
```yaml
|
||||
# docker-compose.yml
|
||||
services:
|
||||
app:
|
||||
deploy:
|
||||
resources:
|
||||
limits:
|
||||
memory: 512M
|
||||
reservations:
|
||||
memory: 256M
|
||||
```
|
||||
|
||||
## Important Notes
|
||||
|
||||
- **Override Location Change**: The `docker-compose.override.yml` file has moved from
|
||||
the project root to `.docker/compose/`. Update your local workflows accordingly.
|
||||
- Personal override files (`.docker/compose/docker-compose.override.yml`) are gitignored
|
||||
and should contain machine-specific configurations only.
|
||||
|
||||
## Next Steps
|
||||
|
||||
- Configure your first proxy host via UI
|
||||
- Enable automatic HTTPS (happens automatically)
|
||||
- Add authentication (Issue #7)
|
||||
- Integrate CrowdSec (Issue #15)
|
||||
50
.docker/compose/README.md
Normal file
50
.docker/compose/README.md
Normal file
@@ -0,0 +1,50 @@
|
||||
# Docker Compose Files
|
||||
|
||||
This directory contains all Docker Compose configuration variants for Charon.
|
||||
|
||||
## File Descriptions
|
||||
|
||||
| File | Purpose |
|
||||
|------|---------|
|
||||
| `docker-compose.yml` | Main production compose configuration. Base services and production settings. |
|
||||
| `docker-compose.dev.yml` | Development overrides. Enables hot-reload, debug logging, and development tools. |
|
||||
| `docker-compose.local.yml` | Local development configuration. Standalone setup for local testing. |
|
||||
| `docker-compose.remote.yml` | Remote deployment configuration. Settings for deploying to remote servers. |
|
||||
| `docker-compose.override.yml` | Personal local overrides. **Gitignored** - use for machine-specific settings. |
|
||||
|
||||
## Usage Patterns
|
||||
|
||||
### Production Deployment
|
||||
|
||||
```bash
|
||||
docker compose -f .docker/compose/docker-compose.yml up -d
|
||||
```
|
||||
|
||||
### Development Mode
|
||||
|
||||
```bash
|
||||
docker compose -f .docker/compose/docker-compose.yml \
|
||||
-f .docker/compose/docker-compose.dev.yml up -d
|
||||
```
|
||||
|
||||
### Local Testing
|
||||
|
||||
```bash
|
||||
docker compose -f .docker/compose/docker-compose.local.yml up -d
|
||||
```
|
||||
|
||||
### With Personal Overrides
|
||||
|
||||
Create your own `docker-compose.override.yml` in this directory for personal
|
||||
configurations (port mappings, volume paths, etc.). This file is gitignored.
|
||||
|
||||
```bash
|
||||
docker compose -f .docker/compose/docker-compose.yml \
|
||||
-f .docker/compose/docker-compose.override.yml up -d
|
||||
```
|
||||
|
||||
## Notes
|
||||
|
||||
- Always use the `-f` flag to specify compose file paths from the project root
|
||||
- The override file is automatically ignored by git - do not commit personal settings
|
||||
- See project tasks in VS Code for convenient pre-configured commands
|
||||
46
.docker/compose/docker-compose.dev.yml
Normal file
46
.docker/compose/docker-compose.dev.yml
Normal file
@@ -0,0 +1,46 @@
|
||||
# Development override - use with: docker-compose -f docker-compose.yml -f docker-compose.dev.yml up
|
||||
|
||||
services:
|
||||
app:
|
||||
# Override for local testing:
|
||||
# CHARON_DEV_IMAGE=ghcr.io/wikid82/charon:dev
|
||||
image: wikid82/charon:dev
|
||||
# Development: expose Caddy admin API externally for debugging
|
||||
ports:
|
||||
- "80:80"
|
||||
- "443:443"
|
||||
- "443:443/udp"
|
||||
- "8080:8080"
|
||||
- "2019:2019" # Caddy admin API (dev only)
|
||||
environment:
|
||||
- CHARON_ENV=development
|
||||
- CPM_ENV=development
|
||||
- CHARON_HTTP_PORT=8080
|
||||
- CPM_HTTP_PORT=80
|
||||
# Generate with: openssl rand -base64 32
|
||||
- CHARON_ENCRYPTION_KEY=your-32-byte-base64-key-here
|
||||
- CHARON_DB_PATH=/app/data/charon.db
|
||||
- CHARON_FRONTEND_DIR=/app/frontend/dist
|
||||
- CHARON_CADDY_ADMIN_API=http://localhost:2019
|
||||
- CHARON_CADDY_CONFIG_DIR=/app/data/caddy
|
||||
# Security Services (Optional)
|
||||
# 🚨 DEPRECATED: Use GUI toggle in Security dashboard instead
|
||||
#- CPM_SECURITY_CROWDSEC_MODE=disabled # ⚠️ DEPRECATED
|
||||
#- CPM_SECURITY_CROWDSEC_API_URL= # ⚠️ DEPRECATED
|
||||
#- CPM_SECURITY_CROWDSEC_API_KEY= # ⚠️ DEPRECATED
|
||||
#- CPM_SECURITY_WAF_MODE=disabled
|
||||
#- CPM_SECURITY_RATELIMIT_ENABLED=false
|
||||
#- CPM_SECURITY_ACL_ENABLED=false
|
||||
- FEATURE_CERBERUS_ENABLED=true
|
||||
# Docker socket group access: copy docker-compose.override.example.yml
|
||||
# to docker-compose.override.yml and set your host's docker GID.
|
||||
volumes:
|
||||
- /var/run/docker.sock:/var/run/docker.sock:ro # For local container discovery
|
||||
- crowdsec_data:/app/data/crowdsec
|
||||
# Mount your existing Caddyfile for automatic import (optional)
|
||||
# - ./my-existing-Caddyfile:/import/Caddyfile:ro
|
||||
# - ./sites:/import/sites:ro # If your Caddyfile imports other files
|
||||
|
||||
volumes:
|
||||
crowdsec_data:
|
||||
driver: local
|
||||
@@ -0,0 +1,4 @@
|
||||
services:
|
||||
charon-e2e:
|
||||
environment:
|
||||
- CHARON_SECURITY_CERBERUS_ENABLED=false
|
||||
66
.docker/compose/docker-compose.local.yml
Normal file
66
.docker/compose/docker-compose.local.yml
Normal file
@@ -0,0 +1,66 @@
|
||||
services:
|
||||
charon:
|
||||
image: charon:local
|
||||
container_name: charon
|
||||
restart: unless-stopped
|
||||
ports:
|
||||
- "80:80" # HTTP (Caddy proxy)
|
||||
- "443:443" # HTTPS (Caddy proxy)
|
||||
- "443:443/udp" # HTTP/3 (Caddy proxy)
|
||||
- "8080:8080" # Management UI (Charon)
|
||||
- "2345:2345" # Delve Debugger
|
||||
environment:
|
||||
- CHARON_ENV=development
|
||||
- CHARON_DEBUG=1
|
||||
- TZ=America/New_York
|
||||
# Generate with: openssl rand -base64 32
|
||||
- CHARON_ENCRYPTION_KEY=your-32-byte-base64-key-here
|
||||
- CHARON_HTTP_PORT=8080
|
||||
- CHARON_DB_PATH=/app/data/charon.db
|
||||
- CHARON_FRONTEND_DIR=/app/frontend/dist
|
||||
- CHARON_CADDY_ADMIN_API=http://localhost:2019
|
||||
- CHARON_CADDY_CONFIG_DIR=/app/data/caddy
|
||||
- CHARON_CADDY_BINARY=caddy
|
||||
- CHARON_IMPORT_CADDYFILE=/import/Caddyfile
|
||||
- CHARON_IMPORT_DIR=/app/data/imports
|
||||
- CHARON_ACME_STAGING=false
|
||||
- FEATURE_CERBERUS_ENABLED=true
|
||||
# Emergency "break-glass" token for security reset when ACL blocks access
|
||||
- CHARON_EMERGENCY_TOKEN=03e4682c1164f0c1cb8e17c99bd1a2d9156b59824dde41af3bb67c513e5c5e92
|
||||
# Docker socket group access: copy docker-compose.override.example.yml
|
||||
# to docker-compose.override.yml and set your host's docker GID.
|
||||
extra_hosts:
|
||||
- "host.docker.internal:host-gateway"
|
||||
cap_add:
|
||||
- SYS_PTRACE
|
||||
security_opt:
|
||||
- seccomp:unconfined
|
||||
volumes:
|
||||
- charon_data:/app/data
|
||||
- caddy_data:/data
|
||||
- caddy_config:/config
|
||||
- crowdsec_data:/app/data/crowdsec
|
||||
- plugins_data:/app/plugins # Read-write for development/hot-loading
|
||||
- /var/run/docker.sock:/var/run/docker.sock:ro # For local container discovery
|
||||
- ./backend:/app/backend:ro # Mount source for debugging
|
||||
# Mount your existing Caddyfile for automatic import (optional)
|
||||
# - <PATH_TO_YOUR_CADDYFILE>:/import/Caddyfile:ro
|
||||
# - <PATH_TO_YOUR_SITES_DIR>:/import/sites:ro # If your Caddyfile imports other files
|
||||
healthcheck:
|
||||
test: ["CMD-SHELL", "curl -fsS http://localhost:8080/api/v1/health || exit 1"]
|
||||
interval: 30s
|
||||
timeout: 10s
|
||||
retries: 3
|
||||
start_period: 40s
|
||||
|
||||
volumes:
|
||||
charon_data:
|
||||
driver: local
|
||||
caddy_data:
|
||||
driver: local
|
||||
caddy_config:
|
||||
driver: local
|
||||
crowdsec_data:
|
||||
driver: local
|
||||
plugins_data:
|
||||
driver: local
|
||||
26
.docker/compose/docker-compose.override.example.yml
Normal file
26
.docker/compose/docker-compose.override.example.yml
Normal file
@@ -0,0 +1,26 @@
|
||||
# Docker Compose override — copy to docker-compose.override.yml to activate.
|
||||
#
|
||||
# Use case: grant the container access to the host Docker socket so that
|
||||
# Charon can discover running containers.
|
||||
#
|
||||
# 1. cp docker-compose.override.example.yml docker-compose.override.yml
|
||||
# 2. Uncomment the service that matches your compose file:
|
||||
# - "charon" for docker-compose.local.yml
|
||||
# - "app" for docker-compose.dev.yml
|
||||
# 3. Replace <GID> with the output of: stat -c '%g' /var/run/docker.sock
|
||||
# 4. docker compose up -d
|
||||
|
||||
services:
|
||||
# Uncomment for docker-compose.local.yml
|
||||
charon:
|
||||
group_add:
|
||||
- "<GID>" # e.g. "988" — run: stat -c '%g' /var/run/docker.sock
|
||||
volumes:
|
||||
- /var/run/docker.sock:/var/run/docker.sock:ro
|
||||
|
||||
# Uncomment for docker-compose.dev.yml
|
||||
app:
|
||||
group_add:
|
||||
- "<GID>" # e.g. "988" — run: stat -c '%g' /var/run/docker.sock
|
||||
volumes:
|
||||
- /var/run/docker.sock:/var/run/docker.sock:ro
|
||||
160
.docker/compose/docker-compose.playwright-ci.yml
Normal file
160
.docker/compose/docker-compose.playwright-ci.yml
Normal file
@@ -0,0 +1,160 @@
|
||||
# Playwright E2E Test Environment for CI/CD
|
||||
# ==========================================
|
||||
# This configuration is specifically designed for GitHub Actions CI/CD pipelines.
|
||||
# Environment variables are provided via GitHub Secrets and generated dynamically.
|
||||
#
|
||||
# DO NOT USE env_file - CI provides variables via $GITHUB_ENV:
|
||||
# - CHARON_ENCRYPTION_KEY: Generated with openssl rand -base64 32 (ephemeral)
|
||||
# - CHARON_EMERGENCY_TOKEN: From repository secrets (secure)
|
||||
#
|
||||
# Usage in CI:
|
||||
# export CHARON_ENCRYPTION_KEY=$(openssl rand -base64 32)
|
||||
# export CHARON_EMERGENCY_TOKEN="${{ secrets.CHARON_EMERGENCY_TOKEN }}"
|
||||
# docker compose -f .docker/compose/docker-compose.playwright-ci.yml up -d
|
||||
#
|
||||
# Profiles:
|
||||
# # Start with security testing services (CrowdSec)
|
||||
# docker compose -f .docker/compose/docker-compose.playwright-ci.yml --profile security-tests up -d
|
||||
#
|
||||
# # Start with notification testing services (MailHog)
|
||||
# docker compose -f .docker/compose/docker-compose.playwright-ci.yml --profile notification-tests up -d
|
||||
#
|
||||
# The setup API will be available since no users exist in the fresh database.
|
||||
# The auth.setup.ts fixture will create a test admin user automatically.
|
||||
|
||||
services:
|
||||
# =============================================================================
|
||||
# Charon Application - Core E2E Testing Service
|
||||
# =============================================================================
|
||||
charon-app:
|
||||
# CI provides CHARON_E2E_IMAGE_TAG=charon:e2e-test (retagged from shared digest)
|
||||
# Local development uses the default fallback value
|
||||
image: ${CHARON_E2E_IMAGE_TAG:-charon:e2e-test}
|
||||
container_name: charon-playwright
|
||||
restart: "no"
|
||||
# CI generates CHARON_ENCRYPTION_KEY dynamically in GitHub Actions workflow
|
||||
# and passes CHARON_EMERGENCY_TOKEN from GitHub Secrets via $GITHUB_ENV.
|
||||
# No .env file is used in CI as it's gitignored and not available.
|
||||
ports:
|
||||
- "8080:8080" # Management UI (Charon)
|
||||
- "127.0.0.1:2019:2019" # Caddy admin API (IPv4 loopback)
|
||||
- "[::1]:2019:2019" # Caddy admin API (IPv6 loopback)
|
||||
- "2020:2020" # Emergency tier-2 API (all interfaces for E2E tests)
|
||||
- "80:80" # Caddy proxy (all interfaces for E2E tests)
|
||||
- "443:443" # Caddy proxy HTTPS (all interfaces for E2E tests)
|
||||
environment:
|
||||
# Core configuration
|
||||
- CHARON_ENV=test
|
||||
- CHARON_DEBUG=0
|
||||
- TZ=UTC
|
||||
# E2E testing encryption key - 32 bytes base64 encoded (not for production!)
|
||||
# Encryption key - MUST be provided via environment variable
|
||||
# Generate with: export CHARON_ENCRYPTION_KEY=$(openssl rand -base64 32)
|
||||
- CHARON_ENCRYPTION_KEY=${CHARON_ENCRYPTION_KEY:?CHARON_ENCRYPTION_KEY is required}
|
||||
# Emergency reset token - for break-glass recovery when locked out by ACL
|
||||
# Generate with: openssl rand -hex 32
|
||||
- CHARON_EMERGENCY_TOKEN=${CHARON_EMERGENCY_TOKEN:-test-emergency-token-for-e2e-32chars}
|
||||
- CHARON_EMERGENCY_SERVER_ENABLED=true
|
||||
- CHARON_SECURITY_TESTS_ENABLED=${CHARON_SECURITY_TESTS_ENABLED:-true}
|
||||
# Emergency server must bind to 0.0.0.0 for Docker port mapping to work
|
||||
# Host binding via compose restricts external access (127.0.0.1:2020:2020)
|
||||
- CHARON_EMERGENCY_BIND=0.0.0.0:2020
|
||||
# Emergency server Basic Auth (required for E2E tests)
|
||||
- CHARON_EMERGENCY_USERNAME=admin
|
||||
- CHARON_EMERGENCY_PASSWORD=changeme
|
||||
# Server settings
|
||||
- CHARON_HTTP_PORT=8080
|
||||
- CHARON_DB_PATH=/app/data/charon.db
|
||||
- CHARON_FRONTEND_DIR=/app/frontend/dist
|
||||
# Caddy settings
|
||||
- CHARON_CADDY_ADMIN_API=http://localhost:2019
|
||||
- CHARON_CADDY_CONFIG_DIR=/app/data/caddy
|
||||
- CHARON_CADDY_BINARY=caddy
|
||||
# ACME settings (staging for E2E tests)
|
||||
- CHARON_ACME_STAGING=true
|
||||
# Security features - disabled by default for faster tests
|
||||
# Enable via profile: --profile security-tests
|
||||
# FEATURE_CERBERUS_ENABLED deprecated - Cerberus enabled by default
|
||||
- CHARON_SECURITY_CROWDSEC_MODE=disabled
|
||||
# SMTP for notification tests (connects to MailHog when profile enabled)
|
||||
- CHARON_SMTP_HOST=mailhog
|
||||
- CHARON_SMTP_PORT=1025
|
||||
- CHARON_SMTP_AUTH=false
|
||||
volumes:
|
||||
# Named volume for test data persistence during test runs
|
||||
- playwright_data:/app/data
|
||||
- playwright_caddy_data:/data
|
||||
- playwright_caddy_config:/config
|
||||
- /var/run/docker.sock:/var/run/docker.sock:ro # For container discovery in tests
|
||||
healthcheck:
|
||||
test: ["CMD", "curl", "-sf", "http://localhost:8080/api/v1/health"]
|
||||
interval: 5s
|
||||
timeout: 3s
|
||||
retries: 12
|
||||
start_period: 10s
|
||||
networks:
|
||||
- playwright-network
|
||||
|
||||
# =============================================================================
|
||||
# CrowdSec - Security Testing Service (Optional Profile)
|
||||
# =============================================================================
|
||||
crowdsec:
|
||||
image: crowdsecurity/crowdsec:latest@sha256:63b595fef92de1778573b375897a45dd226637ee9a3d3db9f57ac7355c369493
|
||||
container_name: charon-playwright-crowdsec
|
||||
profiles:
|
||||
- security-tests
|
||||
restart: "no"
|
||||
environment:
|
||||
- COLLECTIONS=crowdsecurity/nginx crowdsecurity/http-cve
|
||||
- BOUNCER_KEY_charon=test-bouncer-key-for-e2e
|
||||
# Disable online features for isolated testing
|
||||
- DISABLE_ONLINE_API=true
|
||||
volumes:
|
||||
- playwright_crowdsec_data:/var/lib/crowdsec/data
|
||||
- playwright_crowdsec_config:/etc/crowdsec
|
||||
- /var/run/docker.sock:/var/run/docker.sock:ro # For container discovery in tests
|
||||
healthcheck:
|
||||
test: ["CMD", "cscli", "version"]
|
||||
interval: 10s
|
||||
timeout: 5s
|
||||
retries: 5
|
||||
start_period: 30s
|
||||
networks:
|
||||
- playwright-network
|
||||
|
||||
# =============================================================================
|
||||
# MailHog - Email Testing Service (Optional Profile)
|
||||
# =============================================================================
|
||||
mailhog:
|
||||
image: mailhog/mailhog:latest@sha256:8d76a3d4ffa32a3661311944007a415332c4bb855657f4f6c57996405c009bea
|
||||
container_name: charon-playwright-mailhog
|
||||
profiles:
|
||||
- notification-tests
|
||||
restart: "no"
|
||||
ports:
|
||||
- "1025:1025" # SMTP server
|
||||
- "8025:8025" # Web UI for viewing emails
|
||||
networks:
|
||||
- playwright-network
|
||||
|
||||
# =============================================================================
|
||||
# Named Volumes
|
||||
# =============================================================================
|
||||
volumes:
|
||||
playwright_data:
|
||||
driver: local
|
||||
playwright_caddy_data:
|
||||
driver: local
|
||||
playwright_caddy_config:
|
||||
driver: local
|
||||
playwright_crowdsec_data:
|
||||
driver: local
|
||||
playwright_crowdsec_config:
|
||||
driver: local
|
||||
|
||||
# =============================================================================
|
||||
# Networks
|
||||
# =============================================================================
|
||||
networks:
|
||||
playwright-network:
|
||||
driver: bridge
|
||||
59
.docker/compose/docker-compose.playwright-local.yml
Normal file
59
.docker/compose/docker-compose.playwright-local.yml
Normal file
@@ -0,0 +1,59 @@
|
||||
# Docker Compose for Local E2E Testing
|
||||
#
|
||||
# This configuration runs Charon with a fresh, isolated database specifically for
|
||||
# Playwright E2E tests during local development. Uses .env file for credentials.
|
||||
#
|
||||
# Usage:
|
||||
# docker compose -f .docker/compose/docker-compose.playwright-local.yml up -d
|
||||
#
|
||||
# Prerequisites:
|
||||
# - Create .env file in project root with CHARON_ENCRYPTION_KEY and CHARON_EMERGENCY_TOKEN
|
||||
# - Build image: docker build -t charon:local .
|
||||
#
|
||||
# The setup API will be available since no users exist in the fresh database.
|
||||
# The auth.setup.ts fixture will create a test admin user automatically.
|
||||
|
||||
services:
|
||||
charon-e2e:
|
||||
image: charon:local
|
||||
container_name: charon-e2e
|
||||
restart: "no"
|
||||
env_file:
|
||||
- ../../.env
|
||||
ports:
|
||||
- "8080:8080" # Management UI (Charon) - E2E tests verify UI/UX here
|
||||
- "127.0.0.1:2019:2019" # Caddy admin API (read-only status; keep loopback only)
|
||||
- "[::1]:2019:2019" # Caddy admin API (IPv6 loopback)
|
||||
- "2020:2020" # Emergency tier-2 API (all interfaces for E2E tests)
|
||||
# Port 80/443: NOT exposed - middleware testing done via integration tests
|
||||
environment:
|
||||
- CHARON_ENV=e2e # Enable lenient rate limiting (50 attempts/min) for E2E tests
|
||||
- CHARON_DEBUG=0
|
||||
- TZ=UTC
|
||||
# Encryption key and emergency token loaded from env_file (../../.env)
|
||||
# DO NOT add them here - env_file takes precedence and explicit entries override with empty values
|
||||
# Emergency server (Tier 2 break glass) - separate port bypassing all security
|
||||
- CHARON_EMERGENCY_SERVER_ENABLED=true
|
||||
- CHARON_EMERGENCY_BIND=0.0.0.0:2020 # Bind to all interfaces in container (avoid Caddy's 2019)
|
||||
- CHARON_EMERGENCY_USERNAME=admin
|
||||
- CHARON_EMERGENCY_PASSWORD=${CHARON_EMERGENCY_PASSWORD:-changeme}
|
||||
- CHARON_HTTP_PORT=8080
|
||||
- CHARON_DB_PATH=/app/data/charon.db
|
||||
- CHARON_FRONTEND_DIR=/app/frontend/dist
|
||||
- CHARON_CADDY_ADMIN_API=http://localhost:2019
|
||||
- CHARON_CADDY_CONFIG_DIR=/app/data/caddy
|
||||
- CHARON_CADDY_BINARY=caddy
|
||||
- CHARON_ACME_STAGING=true
|
||||
# FEATURE_CERBERUS_ENABLED deprecated - Cerberus enabled by default
|
||||
tmpfs:
|
||||
# True tmpfs for E2E test data - fresh on every run, in-memory only
|
||||
# mode=1777 allows any user to write (container runs as non-root)
|
||||
- /app/data:size=100M,mode=1777
|
||||
volumes:
|
||||
- /var/run/docker.sock:/var/run/docker.sock:ro # For container discovery in tests
|
||||
healthcheck:
|
||||
test: ["CMD-SHELL", "curl -fsS http://localhost:8080/api/v1/health || exit 1"]
|
||||
interval: 5s
|
||||
timeout: 5s
|
||||
retries: 10
|
||||
start_period: 10s
|
||||
19
.docker/compose/docker-compose.remote.yml
Normal file
19
.docker/compose/docker-compose.remote.yml
Normal file
@@ -0,0 +1,19 @@
|
||||
version: '3.9'
|
||||
|
||||
services:
|
||||
# Run this service on your REMOTE servers (not the one running Charon)
|
||||
# to allow Charon to discover containers running there (legacy: CPMP).
|
||||
docker-socket-proxy:
|
||||
image: alpine/socat:latest
|
||||
container_name: docker-socket-proxy
|
||||
restart: unless-stopped
|
||||
ports:
|
||||
# Expose port 2375.
|
||||
# ⚠️ SECURITY WARNING: Ensure this port is NOT accessible from the public internet!
|
||||
# Use a VPN (Tailscale, WireGuard) or a private local network (LAN).
|
||||
- "2375:2375"
|
||||
volumes:
|
||||
# Give the proxy access to the host's Docker socket
|
||||
- /var/run/docker.sock:/var/run/docker.sock:ro
|
||||
# Forward TCP traffic from port 2375 to the internal Docker socket
|
||||
command: tcp-listen:2375,fork,reuseaddr unix-connect:/var/run/docker.sock
|
||||
71
.docker/compose/docker-compose.yml
Normal file
71
.docker/compose/docker-compose.yml
Normal file
@@ -0,0 +1,71 @@
|
||||
services:
|
||||
charon:
|
||||
# Override for local testing:
|
||||
# CHARON_IMAGE=ghcr.io/wikid82/charon:latest
|
||||
image: wikid82/charon:latest
|
||||
container_name: charon
|
||||
restart: unless-stopped
|
||||
ports:
|
||||
- "80:80" # HTTP (Caddy proxy)
|
||||
- "443:443" # HTTPS (Caddy proxy)
|
||||
- "443:443/udp" # HTTP/3 (Caddy proxy)
|
||||
- "8080:8080" # Management UI (Charon)
|
||||
# Emergency server port - ONLY expose via SSH tunnel or VPN for security
|
||||
# Uncomment ONLY if you need localhost access on host machine:
|
||||
# - "127.0.0.1:2020:2020" # Emergency server Tier-2 (localhost-only, avoids Caddy's 2019)
|
||||
environment:
|
||||
- CHARON_ENV=production # CHARON_ preferred; CPM_ values still supported
|
||||
- TZ=UTC # Set timezone (e.g., America/New_York)
|
||||
# Generate with: openssl rand -base64 32
|
||||
- CHARON_ENCRYPTION_KEY=your-32-byte-base64-key-here
|
||||
# Emergency break glass configuration (Tier 1 & Tier 2)
|
||||
# Tier 1: Emergency token for Layer 7 bypass within application
|
||||
# Generate with: openssl rand -hex 32
|
||||
# - CHARON_EMERGENCY_TOKEN=${CHARON_EMERGENCY_TOKEN} # Store in secrets manager
|
||||
# Tier 2: Emergency server on separate port (bypasses Caddy/CrowdSec entirely)
|
||||
# - CHARON_EMERGENCY_SERVER_ENABLED=false # Disabled by default
|
||||
# - CHARON_EMERGENCY_BIND=127.0.0.1:2020 # Localhost only (port 2020 avoids Caddy admin API)
|
||||
# - CHARON_EMERGENCY_USERNAME=admin
|
||||
# - CHARON_EMERGENCY_PASSWORD=${EMERGENCY_PASSWORD} # Store in secrets manager
|
||||
- CHARON_HTTP_PORT=8080
|
||||
- CHARON_DB_PATH=/app/data/charon.db
|
||||
- CHARON_FRONTEND_DIR=/app/frontend/dist
|
||||
- CHARON_CADDY_ADMIN_API=http://localhost:2019
|
||||
- CHARON_CADDY_CONFIG_DIR=/app/data/caddy
|
||||
- CHARON_CADDY_BINARY=caddy
|
||||
- CHARON_IMPORT_CADDYFILE=/import/Caddyfile
|
||||
- CHARON_IMPORT_DIR=/app/data/imports
|
||||
# Paste your CrowdSec API details here to prevent auto reregistration on startup
|
||||
# Obtained from your CrowdSec settings on first setup
|
||||
- CHARON_SECURITY_CROWDSEC_API_URL=http://localhost:8085
|
||||
- CHARON_SECURITY_CROWDSEC_API_KEY=<your-crowdsec-api-key-here>
|
||||
extra_hosts:
|
||||
- "host.docker.internal:host-gateway"
|
||||
volumes:
|
||||
- cpm_data:/app/data # existing data (legacy name); charon will also use this path by default for backward compatibility
|
||||
- caddy_data:/data
|
||||
- caddy_config:/config
|
||||
- crowdsec_data:/app/data/crowdsec
|
||||
- plugins_data:/app/plugins:ro # Read-only in production for security
|
||||
- /var/run/docker.sock:/var/run/docker.sock:ro # For local container discovery
|
||||
# Mount your existing Caddyfile for automatic import (optional)
|
||||
# - ./my-existing-Caddyfile:/import/Caddyfile:ro
|
||||
# - ./sites:/import/sites:ro # If your Caddyfile imports other files
|
||||
healthcheck:
|
||||
test: ["CMD-SHELL", "curl -fsS http://localhost:8080/api/v1/health || exit 1"]
|
||||
interval: 30s
|
||||
timeout: 10s
|
||||
retries: 3
|
||||
start_period: 40s
|
||||
|
||||
volumes:
|
||||
cpm_data:
|
||||
driver: local
|
||||
caddy_data:
|
||||
driver: local
|
||||
caddy_config:
|
||||
driver: local
|
||||
crowdsec_data:
|
||||
driver: local
|
||||
plugins_data:
|
||||
driver: local
|
||||
439
.docker/docker-entrypoint.sh
Executable file
439
.docker/docker-entrypoint.sh
Executable file
@@ -0,0 +1,439 @@
|
||||
#!/bin/sh
|
||||
set -e
|
||||
|
||||
# Entrypoint script to run both Caddy and Charon in a single container
|
||||
# This simplifies deployment for home users
|
||||
|
||||
echo "Starting Charon with integrated Caddy..."
|
||||
|
||||
is_root() {
|
||||
[ "$(id -u)" -eq 0 ]
|
||||
}
|
||||
|
||||
run_as_charon() {
|
||||
if is_root; then
|
||||
gosu charon "$@"
|
||||
else
|
||||
"$@"
|
||||
fi
|
||||
}
|
||||
|
||||
get_group_by_gid() {
|
||||
if command -v getent >/dev/null 2>&1; then
|
||||
getent group "$1" 2>/dev/null || true
|
||||
else
|
||||
awk -F: -v gid="$1" '$3==gid {print $0}' /etc/group 2>/dev/null || true
|
||||
fi
|
||||
}
|
||||
|
||||
create_group_with_gid() {
|
||||
if command -v addgroup >/dev/null 2>&1; then
|
||||
addgroup -g "$1" "$2" 2>/dev/null || true
|
||||
return
|
||||
fi
|
||||
|
||||
if command -v groupadd >/dev/null 2>&1; then
|
||||
groupadd -g "$1" "$2" 2>/dev/null || true
|
||||
fi
|
||||
}
|
||||
|
||||
add_user_to_group() {
|
||||
if command -v addgroup >/dev/null 2>&1; then
|
||||
addgroup "$1" "$2" 2>/dev/null || true
|
||||
return
|
||||
fi
|
||||
|
||||
if command -v usermod >/dev/null 2>&1; then
|
||||
usermod -aG "$2" "$1" 2>/dev/null || true
|
||||
fi
|
||||
}
|
||||
|
||||
# ============================================================================
|
||||
# Volume Permission Handling for Non-Root User
|
||||
# ============================================================================
|
||||
# When running as non-root user (charon), mounted volumes may have incorrect
|
||||
# permissions. This section ensures the application can write to required paths.
|
||||
# Note: This runs as the charon user, so we can only fix owned directories.
|
||||
|
||||
# Ensure /app/data exists and is writable (primary data volume)
|
||||
if [ ! -w "/app/data" ] 2>/dev/null; then
|
||||
echo "Warning: /app/data is not writable. Please ensure volume permissions are correct."
|
||||
echo " Run: docker run ... -v charon_data:/app/data ..."
|
||||
echo " Or fix permissions: chown -R 1000:1000 /path/to/volume"
|
||||
fi
|
||||
|
||||
# Ensure /config exists and is writable (Caddy config volume)
|
||||
if [ ! -w "/config" ] 2>/dev/null; then
|
||||
echo "Warning: /config is not writable. Please ensure volume permissions are correct."
|
||||
fi
|
||||
|
||||
# Create required subdirectories in writable volumes
|
||||
mkdir -p /app/data/caddy 2>/dev/null || true
|
||||
mkdir -p /app/data/crowdsec 2>/dev/null || true
|
||||
mkdir -p /app/data/geoip 2>/dev/null || true
|
||||
|
||||
# Fix ownership for directories created as root
|
||||
if is_root; then
|
||||
chown -R charon:charon /app/data/caddy 2>/dev/null || true
|
||||
chown -R charon:charon /app/data/crowdsec 2>/dev/null || true
|
||||
chown -R charon:charon /app/data/geoip 2>/dev/null || true
|
||||
fi
|
||||
|
||||
# ============================================================================
|
||||
# Plugin Directory Permission Verification
|
||||
# ============================================================================
|
||||
# The PluginLoaderService requires the plugin directory to NOT be world-writable
|
||||
# (mode 0002 bit must not be set). This is a security requirement to prevent
|
||||
# malicious plugin injection.
|
||||
PLUGINS_DIR="${CHARON_PLUGINS_DIR:-/app/plugins}"
|
||||
if [ -d "$PLUGINS_DIR" ]; then
|
||||
# Check if directory is world-writable (security risk)
|
||||
# Using find -perm -0002 is more robust than stat regex - handles sticky/setgid bits correctly
|
||||
if find "$PLUGINS_DIR" -maxdepth 0 -perm -0002 -print -quit 2>/dev/null | grep -q .; then
|
||||
echo "⚠️ WARNING: Plugin directory $PLUGINS_DIR is world-writable!"
|
||||
echo " This is a security risk - plugins could be injected by any user."
|
||||
echo " Attempting to fix permissions (removing world-writable bit)..."
|
||||
# Use chmod o-w to only remove world-writable, preserving sticky/setgid bits
|
||||
if chmod o-w "$PLUGINS_DIR" 2>/dev/null; then
|
||||
echo " ✓ Fixed: Plugin directory world-writable permission removed"
|
||||
else
|
||||
echo " ✗ ERROR: Cannot fix permissions. Please run: chmod o-w $PLUGINS_DIR"
|
||||
echo " Plugin loading may fail due to insecure permissions."
|
||||
fi
|
||||
else
|
||||
echo "✓ Plugin directory permissions OK: $PLUGINS_DIR"
|
||||
fi
|
||||
else
|
||||
echo "Note: Plugin directory $PLUGINS_DIR does not exist (plugins disabled)"
|
||||
fi
|
||||
|
||||
# ============================================================================
|
||||
# Docker Socket Permission Handling
|
||||
# ============================================================================
|
||||
# The Docker integration feature requires access to the Docker socket.
|
||||
# If the container runs as root, we can auto-align group membership with the
|
||||
# socket GID. If running non-root (default), we cannot modify groups; users
|
||||
# can enable Docker integration by using a compatible GID / --group-add.
|
||||
|
||||
if [ -S "/var/run/docker.sock" ] && is_root; then
|
||||
DOCKER_SOCK_GID=$(stat -c '%g' /var/run/docker.sock 2>/dev/null || echo "")
|
||||
if [ -n "$DOCKER_SOCK_GID" ] && [ "$DOCKER_SOCK_GID" != "0" ]; then
|
||||
# Check if a group with this GID exists
|
||||
GROUP_ENTRY=$(get_group_by_gid "$DOCKER_SOCK_GID")
|
||||
if [ -z "$GROUP_ENTRY" ]; then
|
||||
echo "Docker socket detected (gid=$DOCKER_SOCK_GID) - creating docker group and adding charon user..."
|
||||
# Create docker group with the socket's GID
|
||||
create_group_with_gid "$DOCKER_SOCK_GID" docker
|
||||
# Add charon user to the docker group
|
||||
add_user_to_group charon docker
|
||||
echo "Docker integration enabled for charon user"
|
||||
else
|
||||
# Group exists, just add charon to it
|
||||
GROUP_NAME=$(echo "$GROUP_ENTRY" | cut -d: -f1)
|
||||
echo "Docker socket detected (gid=$DOCKER_SOCK_GID, group=$GROUP_NAME) - adding charon user..."
|
||||
add_user_to_group charon "$GROUP_NAME"
|
||||
echo "Docker integration enabled for charon user"
|
||||
fi
|
||||
fi
|
||||
elif [ -S "/var/run/docker.sock" ]; then
|
||||
DOCKER_SOCK_GID=$(stat -c '%g' /var/run/docker.sock 2>/dev/null || echo "unknown")
|
||||
echo "Note: Docker socket mounted (GID=$DOCKER_SOCK_GID) but container is running non-root; skipping docker.sock group setup."
|
||||
echo " If Docker discovery is needed, add 'group_add: [\"$DOCKER_SOCK_GID\"]' to your compose service."
|
||||
if [ "$DOCKER_SOCK_GID" = "0" ]; then
|
||||
if [ "${ALLOW_DOCKER_SOCK_GID_0:-false}" != "true" ]; then
|
||||
echo "⚠️ WARNING: Docker socket GID is 0 (root group). group_add: [\"0\"] grants root-group access."
|
||||
echo " Set ALLOW_DOCKER_SOCK_GID_0=true to acknowledge this risk."
|
||||
fi
|
||||
fi
|
||||
else
|
||||
echo "Note: Docker socket not found. Docker container discovery will be unavailable."
|
||||
fi
|
||||
|
||||
# ============================================================================
|
||||
# CrowdSec Initialization
|
||||
# ============================================================================
|
||||
# Note: CrowdSec agent is not auto-started. Lifecycle is GUI-controlled via backend handlers.
|
||||
|
||||
# Initialize CrowdSec configuration if cscli is present
|
||||
if command -v cscli >/dev/null; then
|
||||
echo "Initializing CrowdSec configuration..."
|
||||
|
||||
# Define persistent paths
|
||||
CS_PERSIST_DIR="/app/data/crowdsec"
|
||||
CS_CONFIG_DIR="$CS_PERSIST_DIR/config"
|
||||
CS_DATA_DIR="$CS_PERSIST_DIR/data"
|
||||
CS_LOG_DIR="/var/log/crowdsec"
|
||||
|
||||
# Ensure persistent directories exist (within writable volume)
|
||||
mkdir -p "$CS_CONFIG_DIR" 2>/dev/null || echo "Warning: Cannot create $CS_CONFIG_DIR"
|
||||
mkdir -p "$CS_DATA_DIR" 2>/dev/null || echo "Warning: Cannot create $CS_DATA_DIR"
|
||||
mkdir -p "$CS_PERSIST_DIR/hub_cache"
|
||||
|
||||
# ============================================================================
|
||||
# CrowdSec Bouncer Key Persistence Directory
|
||||
# ============================================================================
|
||||
# Create the persistent directory for bouncer key storage.
|
||||
# This directory is inside /app/data which is volume-mounted.
|
||||
# The bouncer key will be stored at /app/data/crowdsec/bouncer_key
|
||||
echo "CrowdSec bouncer key will be stored at: $CS_PERSIST_DIR/bouncer_key"
|
||||
|
||||
# Fix ownership for key directory if running as root
|
||||
if is_root; then
|
||||
chown charon:charon "$CS_PERSIST_DIR" 2>/dev/null || true
|
||||
fi
|
||||
|
||||
# Log directories are created at build time with correct ownership
|
||||
# Only attempt to create if they don't exist (first run scenarios)
|
||||
mkdir -p /var/log/crowdsec 2>/dev/null || true
|
||||
mkdir -p /var/log/caddy 2>/dev/null || true
|
||||
|
||||
# Initialize persistent config if key files are missing
|
||||
if [ ! -f "$CS_CONFIG_DIR/config.yaml" ]; then
|
||||
echo "Initializing persistent CrowdSec configuration..."
|
||||
|
||||
# Check if .dist has content
|
||||
if [ -d "/etc/crowdsec.dist" ] && find /etc/crowdsec.dist -mindepth 1 -maxdepth 1 -print -quit 2>/dev/null | grep -q .; then
|
||||
echo "Copying config from /etc/crowdsec.dist..."
|
||||
if ! cp -r /etc/crowdsec.dist/* "$CS_CONFIG_DIR/"; then
|
||||
echo "ERROR: Failed to copy config from /etc/crowdsec.dist"
|
||||
echo "DEBUG: Contents of /etc/crowdsec.dist:"
|
||||
ls -la /etc/crowdsec.dist/
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Verify critical files were copied
|
||||
if [ ! -f "$CS_CONFIG_DIR/config.yaml" ]; then
|
||||
echo "ERROR: config.yaml was not copied to $CS_CONFIG_DIR"
|
||||
echo "DEBUG: Contents of $CS_CONFIG_DIR after copy:"
|
||||
ls -la "$CS_CONFIG_DIR/"
|
||||
exit 1
|
||||
fi
|
||||
echo "✓ Successfully initialized config from .dist directory"
|
||||
elif [ -d "/etc/crowdsec" ] && [ ! -L "/etc/crowdsec" ] && find /etc/crowdsec -mindepth 1 -maxdepth 1 -print -quit 2>/dev/null | grep -q .; then
|
||||
echo "Copying config from /etc/crowdsec (fallback)..."
|
||||
if ! cp -r /etc/crowdsec/* "$CS_CONFIG_DIR/"; then
|
||||
echo "ERROR: Failed to copy config from /etc/crowdsec (fallback)"
|
||||
exit 1
|
||||
fi
|
||||
echo "✓ Successfully initialized config from /etc/crowdsec"
|
||||
else
|
||||
echo "ERROR: No config source found!"
|
||||
echo "DEBUG: /etc/crowdsec.dist contents:"
|
||||
ls -la /etc/crowdsec.dist/ 2>/dev/null || echo " (directory not found or empty)"
|
||||
echo "DEBUG: /etc/crowdsec contents:"
|
||||
ls -la /etc/crowdsec 2>/dev/null || echo " (directory not found or empty)"
|
||||
exit 1
|
||||
fi
|
||||
else
|
||||
echo "✓ Persistent config already exists: $CS_CONFIG_DIR/config.yaml"
|
||||
fi
|
||||
|
||||
# Verify symlink exists (created at build time)
|
||||
# Note: Symlink is created in Dockerfile as root before switching to non-root user
|
||||
# Non-root users cannot create symlinks in /etc, so this must be done at build time
|
||||
if [ -L "/etc/crowdsec" ]; then
|
||||
echo "CrowdSec config symlink verified: /etc/crowdsec -> $CS_CONFIG_DIR"
|
||||
|
||||
# Verify the symlink target is accessible and has config.yaml
|
||||
if [ ! -f "/etc/crowdsec/config.yaml" ]; then
|
||||
echo "ERROR: /etc/crowdsec/config.yaml is not accessible via symlink"
|
||||
echo "DEBUG: Symlink target verification:"
|
||||
ls -la /etc/crowdsec 2>/dev/null || echo " (symlink broken or missing)"
|
||||
echo "DEBUG: Directory contents:"
|
||||
ls -la "$CS_CONFIG_DIR/" 2>/dev/null | head -10 || echo " (directory not found)"
|
||||
exit 1
|
||||
fi
|
||||
echo "✓ /etc/crowdsec/config.yaml is accessible via symlink"
|
||||
else
|
||||
echo "ERROR: /etc/crowdsec symlink not found"
|
||||
echo "Expected: /etc/crowdsec -> /app/data/crowdsec/config"
|
||||
echo "This indicates a critical build-time issue. Symlink must be created at build time as root."
|
||||
echo "DEBUG: Directory check:"
|
||||
find /etc -mindepth 1 -maxdepth 1 -name '*crowdsec*' -exec ls -ld {} \; 2>/dev/null || echo " (no crowdsec entry found)"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Create/update acquisition config for Caddy logs
|
||||
if [ ! -f "/etc/crowdsec/acquis.yaml" ] || [ ! -s "/etc/crowdsec/acquis.yaml" ]; then
|
||||
echo "Creating acquisition configuration for Caddy logs..."
|
||||
cat > /etc/crowdsec/acquis.yaml << 'ACQUIS_EOF'
|
||||
# Caddy access logs acquisition
|
||||
# CrowdSec will monitor these files for security events
|
||||
source: file
|
||||
filenames:
|
||||
- /var/log/caddy/access.log
|
||||
- /var/log/caddy/*.log
|
||||
labels:
|
||||
type: caddy
|
||||
ACQUIS_EOF
|
||||
fi
|
||||
|
||||
# Ensure hub directory exists in persistent storage
|
||||
mkdir -p /etc/crowdsec/hub
|
||||
|
||||
# Perform variable substitution
|
||||
export CFG=/etc/crowdsec
|
||||
export DATA="$CS_DATA_DIR"
|
||||
export PID=/var/run/crowdsec.pid
|
||||
export LOG="$CS_LOG_DIR/crowdsec.log"
|
||||
|
||||
# Process config.yaml and user.yaml with envsubst
|
||||
# We use a temp file to avoid issues with reading/writing same file
|
||||
for file in /etc/crowdsec/config.yaml /etc/crowdsec/user.yaml; do
|
||||
if [ -f "$file" ]; then
|
||||
envsubst < "$file" > "$file.tmp" && mv "$file.tmp" "$file"
|
||||
chown charon:charon "$file" 2>/dev/null || true
|
||||
fi
|
||||
done
|
||||
|
||||
# Configure CrowdSec LAPI to use port 8085 to avoid conflict with Charon (port 8080)
|
||||
if [ -f "/etc/crowdsec/config.yaml" ]; then
|
||||
sed -i 's|listen_uri: 127.0.0.1:8080|listen_uri: 127.0.0.1:8085|g' /etc/crowdsec/config.yaml
|
||||
sed -i 's|listen_uri: 0.0.0.0:8080|listen_uri: 127.0.0.1:8085|g' /etc/crowdsec/config.yaml
|
||||
fi
|
||||
|
||||
# Update local_api_credentials.yaml to use correct port
|
||||
if [ -f "/etc/crowdsec/local_api_credentials.yaml" ]; then
|
||||
sed -i 's|url: http://127.0.0.1:8080|url: http://127.0.0.1:8085|g' /etc/crowdsec/local_api_credentials.yaml
|
||||
sed -i 's|url: http://localhost:8080|url: http://127.0.0.1:8085|g' /etc/crowdsec/local_api_credentials.yaml
|
||||
fi
|
||||
|
||||
# Fix log directory path (ensure it points to /var/log/crowdsec/ not /var/log/)
|
||||
sed -i 's|log_dir: /var/log/$|log_dir: /var/log/crowdsec/|g' "$CS_CONFIG_DIR/config.yaml"
|
||||
# Also handle case where it might be without trailing slash
|
||||
sed -i 's|log_dir: /var/log$|log_dir: /var/log/crowdsec|g' "$CS_CONFIG_DIR/config.yaml"
|
||||
|
||||
# Verify LAPI configuration was applied correctly
|
||||
if grep -q "listen_uri:.*:8085" "$CS_CONFIG_DIR/config.yaml"; then
|
||||
echo "✓ CrowdSec LAPI configured for port 8085"
|
||||
else
|
||||
echo "✗ WARNING: LAPI port configuration may be incorrect"
|
||||
fi
|
||||
|
||||
# Update hub index to ensure CrowdSec can start
|
||||
if [ ! -f "/etc/crowdsec/hub/.index.json" ]; then
|
||||
echo "Updating CrowdSec hub index..."
|
||||
timeout 60s cscli hub update 2>/dev/null || echo "⚠️ Hub update timed out or failed, continuing..."
|
||||
fi
|
||||
|
||||
# Ensure local machine is registered (auto-heal for volume/config mismatch)
|
||||
# We force registration because we just restored configuration (and likely credentials)
|
||||
echo "Registering local machine..."
|
||||
cscli machines add -a --force 2>/dev/null || echo "Warning: Machine registration may have failed"
|
||||
|
||||
# Install hub items (parsers, scenarios, collections) if local mode enabled
|
||||
if [ "$SECURITY_CROWDSEC_MODE" = "local" ]; then
|
||||
echo "Installing CrowdSec hub items..."
|
||||
if [ -x /usr/local/bin/install_hub_items.sh ]; then
|
||||
/usr/local/bin/install_hub_items.sh 2>/dev/null || echo "Warning: Some hub items may not have installed"
|
||||
fi
|
||||
fi
|
||||
|
||||
# Fix ownership AFTER cscli commands (they run as root and create root-owned files)
|
||||
echo "Fixing CrowdSec file ownership..."
|
||||
if is_root; then
|
||||
chown -R charon:charon /var/lib/crowdsec 2>/dev/null || true
|
||||
chown -R charon:charon /app/data/crowdsec 2>/dev/null || true
|
||||
chown -R charon:charon /var/log/crowdsec 2>/dev/null || true
|
||||
fi
|
||||
fi
|
||||
|
||||
# CrowdSec Lifecycle Management:
|
||||
# CrowdSec configuration is initialized above (symlinks, directories, hub updates)
|
||||
# However, the CrowdSec agent is NOT auto-started in the entrypoint.
|
||||
# Instead, CrowdSec lifecycle is managed by the backend handlers via GUI controls.
|
||||
# This makes CrowdSec consistent with other security features (WAF, ACL, Rate Limiting).
|
||||
# Users enable/disable CrowdSec using the Security dashboard toggle, which calls:
|
||||
# - POST /api/v1/admin/crowdsec/start (to start the agent)
|
||||
# - POST /api/v1/admin/crowdsec/stop (to stop the agent)
|
||||
# This approach provides:
|
||||
# - Consistent user experience across all security features
|
||||
# - No environment variable dependency
|
||||
# - Real-time control without container restart
|
||||
# - Proper integration with Charon's security orchestration
|
||||
echo "CrowdSec configuration initialized. Agent lifecycle is GUI-controlled."
|
||||
|
||||
# Start Caddy in the background with initial empty config
|
||||
# Run Caddy as charon user for security
|
||||
echo '{"admin":{"listen":"0.0.0.0:2019"},"apps":{}}' > /config/caddy.json
|
||||
# Use JSON config directly; no adapter needed
|
||||
run_as_charon caddy run --config /config/caddy.json &
|
||||
CADDY_PID=$!
|
||||
echo "Caddy started (PID: $CADDY_PID)"
|
||||
|
||||
# Wait for Caddy to be ready
|
||||
echo "Waiting for Caddy admin API..."
|
||||
i=1
|
||||
while [ "$i" -le 30 ]; do
|
||||
if curl -sf http://127.0.0.1:2019/config/ > /dev/null 2>&1; then
|
||||
echo "Caddy is ready!"
|
||||
break
|
||||
fi
|
||||
i=$((i+1))
|
||||
sleep 1
|
||||
done
|
||||
|
||||
# Start Charon management application
|
||||
# Drop privileges to charon user before starting the application
|
||||
# This maintains security while allowing Docker socket access via group membership
|
||||
# Note: When running as root, we use gosu; otherwise we run directly.
|
||||
echo "Starting Charon management application..."
|
||||
DEBUG_FLAG=${CHARON_DEBUG:-$CPMP_DEBUG}
|
||||
DEBUG_PORT=${CHARON_DEBUG_PORT:-${CPMP_DEBUG_PORT:-2345}}
|
||||
|
||||
# Determine binary path
|
||||
bin_path=/app/charon
|
||||
if [ ! -f "$bin_path" ]; then
|
||||
bin_path=/app/cpmp
|
||||
fi
|
||||
|
||||
if [ "$DEBUG_FLAG" = "1" ]; then
|
||||
# Check if binary has debug symbols (required for Delve)
|
||||
# objdump -h lists section headers; .debug_info is present if DWARF symbols exist
|
||||
if command -v objdump >/dev/null 2>&1; then
|
||||
if ! objdump -h "$bin_path" 2>/dev/null | grep -q '\.debug_info'; then
|
||||
echo "⚠️ WARNING: Binary lacks debug symbols (DWARF info stripped)."
|
||||
echo " Delve debugging will NOT work with this binary."
|
||||
echo " To fix, rebuild with: docker build --build-arg BUILD_DEBUG=1 ..."
|
||||
echo " Falling back to normal execution (without debugger)."
|
||||
run_as_charon "$bin_path" &
|
||||
else
|
||||
echo "✓ Debug symbols detected. Running Charon under Delve (port $DEBUG_PORT)"
|
||||
run_as_charon /usr/local/bin/dlv exec "$bin_path" --headless --listen=":$DEBUG_PORT" --api-version=2 --accept-multiclient --continue --log -- &
|
||||
fi
|
||||
else
|
||||
# objdump not available, try to run Delve anyway with a warning
|
||||
echo "Note: Cannot verify debug symbols (objdump not found). Attempting Delve..."
|
||||
run_as_charon /usr/local/bin/dlv exec "$bin_path" --headless --listen=":$DEBUG_PORT" --api-version=2 --accept-multiclient --continue --log -- &
|
||||
fi
|
||||
else
|
||||
run_as_charon "$bin_path" &
|
||||
fi
|
||||
APP_PID=$!
|
||||
echo "Charon started (PID: $APP_PID)"
|
||||
shutdown() {
|
||||
echo "Shutting down..."
|
||||
kill -TERM "$APP_PID" 2>/dev/null || true
|
||||
kill -TERM "$CADDY_PID" 2>/dev/null || true
|
||||
# Note: CrowdSec process lifecycle is managed by backend handlers
|
||||
# The backend will handle graceful CrowdSec shutdown when the container stops
|
||||
wait "$APP_PID" 2>/dev/null || true
|
||||
wait "$CADDY_PID" 2>/dev/null || true
|
||||
exit 0
|
||||
}
|
||||
|
||||
# Trap signals for graceful shutdown
|
||||
trap 'shutdown' TERM INT
|
||||
|
||||
echo "Charon is running!"
|
||||
echo " - Management UI: http://localhost:8080"
|
||||
echo " - Caddy Proxy: http://localhost:80, https://localhost:443"
|
||||
echo " - Caddy Admin API: http://localhost:2019"
|
||||
|
||||
# Wait loop: exit when either process dies, then shutdown the other
|
||||
while kill -0 "$APP_PID" 2>/dev/null && kill -0 "$CADDY_PID" 2>/dev/null; do
|
||||
sleep 1
|
||||
done
|
||||
|
||||
echo "A process exited, initiating shutdown..."
|
||||
shutdown
|
||||
Reference in New Issue
Block a user