Files
Charon/docs/plans/container-hardening-fix.md
GitHub Actions 710d729022 chore: replace wget with curl in various scripts for consistency and reliability
- Updated WafConfig.tsx to correct regex for common bad bots.
- Modified cerberus_integration.sh to use curl instead of wget for backend readiness check.
- Changed coraza_integration.sh to utilize curl for checking httpbin backend status.
- Updated crowdsec_startup_test.sh to use curl for LAPI health check.
- Replaced wget with curl in install-go-1.25.5.sh for downloading Go.
- Modified rate_limit_integration.sh to use curl for backend readiness check.
- Updated waf_integration.sh to replace wget with curl for checking httpbin backend status.
2026-01-24 22:22:39 +00:00

360 lines
12 KiB
Markdown

# Container Hardening Configuration Fix Plan
**Issue**: Documentation shows conflicting container hardening configuration with `read_only: true` and volume mounts that may not work correctly.
**Date**: 2025-12-23
## Research Findings: Where Charon Writes Data
### 1. Primary Data Directories (Evidence from code analysis)
#### `/app/data/` - Main persistent data directory
- **Database**: `/app/data/charon.db` (default, configurable via `CHARON_DB_PATH`)
- Source: `backend/internal/config/config.go:44`
- SQLite database file with WAL mode
- Requires write access for database operations
- **Database Backups**: `/app/data/backups/`
- Source: `backend/internal/services/backup_service.go:35`
- Creates timestamped `.zip` backups of database and Caddy data
- Cron job runs daily at 3 AM
- Requires write access for backup creation and cleanup
- **Caddy Configuration**: `/app/data/caddy/`
- Source: `backend/internal/config/config.go:47`
- Stores Caddy TLS certificates (Let's Encrypt, ZeroSSL, custom)
- Certificate storage under subdirectories like `certificates/acme-v02.api.letsencrypt.org-directory/`
- Requires persistent write access for certificate management
- **Import Directory**: `/app/data/imports/`
- Source: `backend/internal/config/config.go:50`
- Used for Caddyfile import functionality
- Requires write access for import operations
- **CrowdSec Data**: `/app/data/crowdsec/`
- Source: `backend/internal/config/config.go:57` and `.docker/docker-entrypoint.sh:96`
- Persistent CrowdSec configuration and data
- `/app/data/crowdsec/config/` - CrowdSec configuration files (symlinked to `/etc/crowdsec`)
- `/app/data/crowdsec/data/` - CrowdSec database and persistent data
- `/app/data/crowdsec/hub_cache/` - Hub item cache
- Requires write access for CrowdSec operation
- **GeoIP Database**: `/app/data/geoip/GeoLite2-Country.mmdb`
- Source: `Dockerfile:255` (downloaded at build time)
- Read-only at runtime (pre-populated in image)
- No write access needed at runtime
#### `/var/log/` - Log files (Requires tmpfs for read-only root)
- **Caddy Logs**: `/var/log/caddy/access.log`
- Source: `backend/internal/caddy/config.go:18` and `configs/crowdsec/acquis.yaml`
- JSON-formatted access logs for HTTP/HTTPS traffic
- CrowdSec monitors this file when enabled
- Requires write access with log rotation
- **CrowdSec Logs**: `/var/log/crowdsec/`
- Source: `.docker/docker-entrypoint.sh:100`
- CrowdSec agent and LAPI logs
- Requires write access when CrowdSec is enabled
#### `/config/` - Caddy runtime configuration (Requires tmpfs for read-only root)
- **Caddy JSON Config**: `/config/caddy.json`
- Source: `.docker/docker-entrypoint.sh:203`
- Runtime Caddy configuration loaded via Admin API
- Generated dynamically by Charon backend
- Requires write access for configuration updates
#### `/tmp/` - Temporary files (Requires tmpfs for read-only root)
- Used by CrowdSec hub operations
- Source: Various test files show `/tmp/buildenv_*` patterns for hub sync
- Requires write access for temporary file operations
#### `/etc/crowdsec` - Symlink to persistent storage
- **Symlink**: `/etc/crowdsec -> /app/data/crowdsec/config`
- Source: `Dockerfile:405` and `.docker/docker-entrypoint.sh:110`
- Created at build time (as root) to allow persistent CrowdSec config
- Symlink itself is read-only at runtime
- Target directory requires write access
#### `/var/lib/crowdsec` - CrowdSec data directory (May require tmpfs)
- **CrowdSec Runtime Data**: `/var/lib/crowdsec/data/`
- Source: `Dockerfile:251` and `.docker/docker-entrypoint.sh:110`
- CrowdSec agent runtime data
- May be symlinked or used for temporary data
- Investigate if this can be redirected to `/app/data/crowdsec/data`
### 2. Docker Socket Access
- **Docker Socket**: `/var/run/docker.sock` (read-only mount)
- Source: `.docker/compose/docker-compose.yml:32`
- Used for container discovery feature
- Read-only access is sufficient
## Current Docker Configuration Analysis
### Volume Mounts in Production (`docker-compose.yml`)
```yaml
volumes:
- cpm_data:/app/data # Persistent database, caddy certs, backups
- caddy_data:/data # Caddy data directory (may be unused?)
- caddy_config:/config # Caddy runtime config (needs write)
- crowdsec_data:/app/data/crowdsec # CrowdSec persistent data
- /var/run/docker.sock:/var/run/docker.sock:ro # Docker integration
```
### Issues with Current Setup
1. **`caddy_data:/data`** - This volume may be unnecessary as Caddy uses `/app/data/caddy/` for certificates
2. **`/config` mount** - Required but may conflict with `read_only: true` root filesystem
3. **`/var/log/`** - Not mounted as tmpfs, will fail with `read_only: true`
4. **`/tmp/`** - Not mounted as tmpfs, will fail with `read_only: true`
5. **`/var/lib/crowdsec`** - May need tmpfs or redirection to `/app/data/crowdsec/data`
## Correct Container Hardening Configuration
### Strategy
1. **Root filesystem**: `read_only: true` for security
2. **Persistent data**: Named volume at `/app/data` for all persistent data
3. **Ephemeral data**: tmpfs mounts for logs, temp files, and runtime config
4. **Symlinks**: Leverage existing symlinks in the image (`/etc/crowdsec -> /app/data/crowdsec/config`)
### Recommended Docker Compose Configuration
```yaml
services:
charon:
image: ghcr.io/wikid82/charon:latest
container_name: charon
restart: unless-stopped
# Security: Read-only root filesystem
read_only: true
# Drop all capabilities except NET_BIND_SERVICE (for ports 80/443)
cap_drop:
- ALL
cap_add:
- NET_BIND_SERVICE
# Prevent privilege escalation
security_opt:
- no-new-privileges:true
ports:
- "80:80"
- "443:443"
- "443:443/udp"
- "8080:8080"
environment:
- CHARON_ENV=production
- TZ=UTC
- 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_CROWDSEC_CONFIG_DIR=/app/data/crowdsec
extra_hosts:
- "host.docker.internal:host-gateway"
volumes:
# Persistent data (database, certificates, backups, CrowdSec config)
- charon_data:/app/data
# Ephemeral tmpfs mounts for writable directories
- type: tmpfs
target: /tmp
tmpfs:
size: 100M
mode: 1777 # Sticky bit for multi-user temp directory
- type: tmpfs
target: /var/log/caddy
tmpfs:
size: 100M
mode: 0755
- type: tmpfs
target: /var/log/crowdsec
tmpfs:
size: 100M
mode: 0755
- type: tmpfs
target: /config
tmpfs:
size: 10M
mode: 0755
- type: tmpfs
target: /var/lib/crowdsec
tmpfs:
size: 50M
mode: 0755
- type: tmpfs
target: /run
tmpfs:
size: 10M
mode: 0755
# Docker socket for container discovery (read-only)
- /var/run/docker.sock:/var/run/docker.sock:ro
# Optional: Import existing Caddyfile (read-only)
# - ./my-existing-Caddyfile:/import/Caddyfile:ro
healthcheck:
test: ["CMD", "curl", "--no-verbose", "--tries=1", "--spider", "http://localhost:8080/api/v1/health"]
interval: 30s
timeout: 10s
retries: 3
start_period: 40s
volumes:
charon_data:
driver: local
```
### Why This Configuration Works
1. **`read_only: true`** - Root filesystem is read-only, preventing unauthorized file modifications
2. **`/app/data` volume** - All persistent data in one place:
- Database (`charon.db`)
- Backups (`backups/`)
- Caddy certificates (`caddy/certificates/`)
- Import directory (`imports/`)
- CrowdSec config and data (`crowdsec/`)
- GeoIP database (pre-populated in image, read-only)
3. **tmpfs mounts** - Ephemeral writable directories:
- `/tmp` - Temporary files (CrowdSec hub operations)
- `/var/log/caddy` - Caddy access logs (monitored by CrowdSec)
- `/var/log/crowdsec` - CrowdSec agent logs
- `/config` - Caddy runtime JSON config
- `/var/lib/crowdsec` - CrowdSec runtime data
- `/run` - Runtime state files and PIDs
4. **Symlinks preserved** - `/etc/crowdsec -> /app/data/crowdsec/config` still works
5. **Capabilities dropped** - Only NET_BIND_SERVICE retained for ports 80/443
6. **No privilege escalation** - `no-new-privileges:true` prevents privilege escalation attacks
### What About the Removed Volumes?
- **`caddy_data:/data`** - Removed, not used by Charon (Caddy stores certs in `/app/data/caddy/`)
- **`caddy_config:/config`** - Replaced with tmpfs (runtime config doesn't need persistence)
- **`crowdsec_data:/app/data/crowdsec`** - Merged into main `charon_data` volume
## Validation Checklist
Before deploying this configuration, validate:
- [ ] Charon starts successfully with `read_only: true`
- [ ] Database operations work (create/read/update/delete)
- [ ] Caddy can obtain and renew TLS certificates
- [ ] Backups are created and restored successfully
- [ ] CrowdSec can start, update hub, and process logs (if enabled)
- [ ] Log files are written to `/var/log/caddy/access.log`
- [ ] Container discovery works with Docker socket
- [ ] Import directory accepts uploaded Caddyfiles
- [ ] No "read-only filesystem" errors in logs
## Testing Steps
1. **Deploy with hardening**:
```bash
docker compose -f .docker/compose/docker-compose.yml down
# Update docker-compose.yml with new configuration
docker compose -f .docker/compose/docker-compose.yml up -d
```
2. **Check startup logs**:
```bash
docker logs charon
```
3. **Test database operations**:
- Create a new proxy host via UI
- Edit an existing proxy host
- Delete a proxy host
- Check `/app/data/charon.db` is updated
4. **Test certificate management**:
- Add a domain with Let's Encrypt
- Verify certificate is obtained
- Check `/app/data/caddy/certificates/` for certificate files
5. **Test backups**:
- Trigger manual backup via UI
- Verify backup appears in `/app/data/backups/`
- Test backup restoration
6. **Test CrowdSec** (if enabled):
- Enable CrowdSec via Security dashboard
- Check CrowdSec starts successfully
- Verify hub items are installed
- Check logs are being processed
7. **Inspect filesystem permissions**:
```bash
docker exec charon ls -la /app/data
docker exec charon ls -la /var/log/caddy
docker exec charon ls -la /config
docker exec charon ls -la /tmp
```
## Rollback Plan
If the hardened configuration causes issues:
1. Remove `read_only: true` from docker-compose.yml
2. Revert to previous volume configuration
3. Restart container: `docker compose restart charon`
## Implementation Plan
1. **Update documentation** in relevant files:
- Update container hardening example with correct configuration
- Add explanation of why each mount is needed
- Document tmpfs size recommendations
2. **Create example file**: `examples/docker-compose.hardened.yml`
- Full working example with read-only root filesystem
- Comments explaining each security feature
- Reference from main documentation
3. **Add to troubleshooting docs**:
- Document common issues with read-only root filesystem
- Explain tmpfs sizing considerations
- Provide tips for debugging permission issues
4. **Test with integration tests**:
- Add integration test that deploys hardened configuration
- Verify all features work correctly
- Include in CI/CD pipeline
## References
- **CIS Docker Benchmark 5.12**: Run containers with a read-only root filesystem
- **CIS Docker Benchmark 5.25**: Restrict container from acquiring additional privileges
- **CIS Docker Benchmark 5.26**: Ensure container health is checked at runtime
- **Code Evidence**:
- Database path: `backend/internal/config/config.go:44`
- Backup service: `backend/internal/services/backup_service.go:35`
- Caddy config: `backend/internal/caddy/config.go:18`
- CrowdSec setup: `.docker/docker-entrypoint.sh:96-206`
- Volume mounts: `.docker/compose/docker-compose.yml:30-36`