360 lines
12 KiB
Markdown
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`
|