Last Updated: 2025-12-17 Security Review Date: 2025-12-17
Ofelia implements defense-in-depth security practices across authentication, input validation, and application stability. This document covers security features, best practices, and deployment considerations for production environments.
Understanding what security controls belong where is critical for proper deployment:
┌─────────────────────────────────────────────────────────────────────┐
│ INFRASTRUCTURE RESPONSIBILITY │
│ (Docker daemon, Kubernetes, host OS, network) │
│ │
│ • Container privileges (--privileged, capabilities) │
│ • Host mounts and volume permissions │
│ • Network isolation and firewall rules │
│ • Resource limits (cgroups, ulimits) │
│ • Security profiles (AppArmor, SELinux, seccomp) │
│ • Docker socket access control │
│ • TLS termination (reverse proxy) │
└─────────────────────────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────────────┐
│ OFELIA RESPONSIBILITY │
│ (Application-level controls) │
│ │
│ • Authentication (tokens, passwords) │
│ • Authorization (who can create/run jobs) - Note: No RBAC yet │
│ • Input format validation (cron syntax, image names) │
│ • Rate limiting for API endpoints │
│ • Session management and token handling │
│ • Application stability (memory bounds, graceful shutdown) │
│ • Audit logging of security events │
└─────────────────────────────────────────────────────────────────────┘
Key Principle: Ofelia schedules jobs as requested. The infrastructure enforces what's permitted. If you need to restrict container privileges, configure your Docker daemon, use rootless Docker, or deploy with Kubernetes PodSecurityStandards. See ADR-002 for the full rationale.
┌─────────────────────────────────────────────────┐
│ External Access Layer │
│ ┌──────────┐ ┌──────────┐ ┌──────────┐ │
│ │ TLS │ │ HTTPS │ │ mTLS │ │
│ └──────────┘ └──────────┘ └──────────┘ │
└─────────────────────────────────────────────────┘
↓
┌─────────────────────────────────────────────────┐
│ Authentication Layer │
│ ┌──────────┐ ┌──────────┐ ┌──────────┐ │
│ │ Token │ │ Bcrypt │ │ CSRF │ │
│ └──────────┘ └──────────┘ └──────────┘ │
└─────────────────────────────────────────────────┘
↓
┌─────────────────────────────────────────────────┐
│ Input Validation Layer │
│ ┌──────────┐ ┌──────────┐ ┌──────────┐ │
│ │Sanitizer │ │Validator │ │ Filters │ │
│ └──────────┘ └──────────┘ └──────────┘ │
└─────────────────────────────────────────────────┘
↓
┌─────────────────────────────────────────────────┐
│ Application Layer │
│ ┌──────────┐ ┌──────────┐ ┌──────────┐ │
│ │ Jobs │ │ API │ │Scheduler │ │
│ └──────────┘ └──────────┘ └──────────┘ │
└─────────────────────────────────────────────────┘
↓
┌─────────────────────────────────────────────────┐
│ Container Isolation Layer │
│ ┌──────────┐ ┌──────────┐ ┌──────────┐ │
│ │ Docker │ │Resources │ │ Networks │ │
│ └──────────┘ └──────────┘ └──────────┘ │
└─────────────────────────────────────────────────┘
Protection: Token-based authentication (single-user model)
-
✅ Secure Authentication (web/auth_secure.go):
- Bcrypt password hashing (cost factor 12)
- Cryptographically secure token generation
- Token expiry enforcement (configurable, default 24 hours)
- Constant-time username comparison
- Rate limiting per IP (default 5 attempts/minute)
- Session management with secure cookies
-
⚠️ Current Limitations:- No RBAC: Single credential model - any authenticated user has full access
- No token revocation list: Tokens valid until expiry (use short expiry in sensitive environments)
- LocalJob restrictions only apply to Docker label sources
-
✅ Access Control:
- API endpoints require valid token (when auth enabled)
- LocalJob execution from labels restricted by default
- Docker socket access delegated to infrastructure
Configuration:
[global]
web-auth-enabled = true
web-username = admin
web-password-hash = $2a$12$... # bcrypt hash
web-secret-key = ${WEB_SECRET_KEY}
web-token-expiry = 24 # hours
allow-host-jobs-from-labels = false # Restrict LocalJobsProtection: Strong cryptography and secure password storage
-
✅ Password Hashing:
// Bcrypt with cost 12 (2^12 iterations) hash, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost)
-
✅ Token Signing:
- HMAC-based token signing with minimum 32-byte secret
- Automatic token key validation on startup
-
✅ Secure Storage:
- Credentials never logged
- Environment variable injection for secrets
- Bcrypt hashing for production passwords
Best Practices:
# Generate bcrypt password hash
python3 -c "import bcrypt; print(bcrypt.hashpw(b'your-password', bcrypt.gensalt(12)).decode())"
# Generate secret key
openssl rand -base64 48
# Store in environment
export OFELIA_WEB_SECRET_KEY="your-generated-secret-here"Protection: Multi-layer input validation and sanitization (config/sanitizer.go)
// Blocked patterns
union select, insert into, update set, delete from, drop table
create table, alter table, exec, executeExample:
sanitizer := config.NewSanitizer()
cleaned, err := sanitizer.SanitizeString(userInput, 1024)
// Removes: union select, <script>, javascript:, etc.// Blocked operators
; & | < > $ ` && || >> << $( ${
// Blocked commands
rm -rf, dd if=, mkfs, format, sudo, su -
wget, curl, nc, telnet, chmod 777Example:
err := sanitizer.ValidateCommand("/backup/script.sh --dry-run")
// Blocks: shell operators, command substitution, dangerous commands// Blocked patterns
../ ..\ ..%2F %2e%2e
// Blocked extensions
.exe .sh .dll .bat .cmdExample:
err := sanitizer.ValidatePath("/var/log/backup.log", "/var/log")
// Blocks: ../../../etc/passwd, URL-encoded traversal// Validates format: [registry/]namespace/repository[:tag][@sha256:digest]
err := sanitizer.ValidateDockerImage("nginx:1.21-alpine")
// Blocks: invalid format, suspicious patterns (.., //)Protection: Security-first architecture
- ✅ Defense in Depth: Multiple security layers (auth, validation, isolation)
- ✅ Fail-Safe Defaults: Secure defaults, explicit opt-in for privileged operations
- ✅ Least Privilege: Minimal permissions, container isolation
- ✅ Separation of Duties: Job types enforce execution boundaries
- ✅ Input Validation: All inputs validated before processing
Secure Design Patterns:
# LocalJobs disabled by default from labels
allow-host-jobs-from-labels = false
# Containers deleted after execution
delete = true
# Overlap prevention for critical jobs
overlap = falseProtection: Secure defaults and configuration validation
-
✅ Secure Defaults:
- Docker events enabled for real-time monitoring
- Container cleanup enabled
- HTTP security headers enforced
- Rate limiting enabled
-
✅ Configuration Validation (config/validator.go):
validator := config.NewValidator() validator.ValidateRequired("web-secret-key", config.WebSecretKey) validator.ValidateCronExpression("schedule", job.Schedule) validator.ValidateEmail("email-to", config.EmailTo)
-
✅ Security Headers (web/middleware.go):
X-Content-Type-Options: nosniff X-Frame-Options: DENY X-XSS-Protection: 1; mode=block Referrer-Policy: strict-origin-when-cross-origin Content-Security-Policy: default-src 'self'; script-src 'self' 'unsafe-inline'; style-src 'self' 'unsafe-inline' Strict-Transport-Security: max-age=31536000 (when HTTPS)
⚠️ Note: CSP allows'unsafe-inline'for scripts and styles to support the embedded web UI. For stricter CSP, deploy behind a reverse proxy with custom headers.
Protection: Dependency management and security updates
-
✅ Go Dependency Management:
# Regular security updates go get -u ./... go mod tidy # Security scanning govulncheck ./...
-
✅ Docker Image Security:
- Base images from official sources
- Regular image updates
- Minimal attack surface (Alpine Linux)
- No unnecessary packages
Security Update Schedule:
- Critical vulnerabilities: Immediate patch
- High severity: Within 7 days
- Medium/Low: Next release cycle
- Dependencies: Monthly review
Protection: Robust authentication mechanisms
-
✅ Authentication Protections:
- Authentication tokens with configurable expiry
- CSRF tokens for state-changing operations
- Rate limiting prevents brute force
-
✅ Session Management (web/auth_secure.go):
// Secure cookies cookie := &http.Cookie{ Name: "auth_token", Value: token, Path: "/", HttpOnly: true, // Prevent JavaScript access Secure: true, // HTTPS only SameSite: http.SameSiteStrictMode, // CSRF protection MaxAge: int(h.tokenManager.tokenExpiry.Seconds()), }
-
✅ Timing Attack Protection:
// Constant-time comparison usernameMatch := subtle.ConstantTimeCompare( []byte(credentials.Username), []byte(h.config.Username) ) == 1 // Delay on authentication failure (100ms) time.Sleep(100 * time.Millisecond)
Protection: Code signing and integrity verification
-
✅ Container Integrity:
- SHA256 image digests supported
- Image signature verification (optional)
- Immutable tags avoided in production
-
✅ Configuration Integrity:
- Configuration validation before loading
- Hash-based change detection
- Atomic configuration updates
Best Practices:
# Use SHA256 digests for production images
image = nginx@sha256:abc123...
# Enable image pull always for latest security patches
pull = alwaysProtection: Comprehensive logging and monitoring
-
✅ Structured Logging (stdlib
log/slog):- All authentication attempts logged
- Failed login tracking
- Command execution logging
- Source location via
AddSource: true
-
✅ Security Event Logging:
logger.WarnWithFields("Authentication failed", map[string]interface{}{ "username": username, "ip": clientIP, "attempt": attemptCount, })
-
✅ Prometheus Metrics (metrics/prometheus.go):
ofelia_http_requests_total{status="401"} # Failed auth ofelia_jobs_failed_total # Failed jobs ofelia_docker_errors_total # Docker errors
Protection: Trust-the-config model with optional host whitelist
Ofelia follows a trust-the-config security model for webhooks: since users can already run arbitrary commands via local/exec jobs, the same trust level applies to webhook destinations. All hosts are allowed by default.
-
✅ Trust Model (middlewares/webhook_security.go):
- If you control the configuration, you control the behavior
- Same trust level as local command execution
- Default:
webhook-allowed-hosts = *(allow all hosts)
-
✅ URL Validation:
- Only
http://andhttps://schemes allowed - URL must have a valid hostname
- Only
-
✅ Optional Whitelist Mode (for multi-tenant/cloud deployments):
- Set specific hosts to enable whitelist mode
- Supports wildcards:
*.example.com
Default (self-hosted/trusted environments):
[global]
# All hosts allowed by default (no config needed)
# webhook-allowed-hosts = *Whitelist Mode (for cloud/multi-tenant deployments):
[global]
# Only allow specific hosts
webhook-allowed-hosts = hooks.slack.com, discord.com, ntfy.internal, 192.168.1.20Implementation: web/auth_secure.go
Features:
- Bcrypt password hashing (cost 12)
- Cryptographically secure token generation
- Constant-time username comparison
- Rate limiting (5 attempts/minute)
- CSRF token protection
- Timing attack prevention
- Secure HTTP-only cookies
Password Hashing:
# Generate bcrypt hash for configuration
python3 -c "import bcrypt; print(bcrypt.hashpw(b'your-password', bcrypt.gensalt(12)).decode())"Usage:
# Login
curl -X POST http://localhost:8081/api/login \
-H "Content-Type: application/json" \
-d '{"username":"admin","password":"your-password"}'
# Response
{
"token": "abc123...",
"csrf_token": "xyz789...",
"expires_in": 86400
}
# Use token
curl -H "Authorization: Bearer abc123..." \
http://localhost:8081/api/jobsRate Limiting:
// 5 attempts per minute per IP
rateLimiter := NewRateLimiter(5, 5)
if !rateLimiter.Allow(clientIP) {
return errors.New("too many authentication attempts")
}CSRF Protection:
// Generate CSRF token (one-time use)
csrfToken, err := tokenManager.GenerateCSRFToken()
// Validate and consume token
valid := tokenManager.ValidateCSRFToken(token)Validator: config/validator.go
Validation Rules:
- Required field validation
- String length validation (min/max)
- Email format validation
- URL format validation
- Cron expression validation
- Numeric range validation
- Path validation
- Enum validation
Example:
validator := config.NewValidator()
// Required fields
validator.ValidateRequired("job-name", jobName)
// Email validation
validator.ValidateEmail("email-to", "admin@example.com")
// Cron expression validation
validator.ValidateCronExpression("schedule", "0 */6 * * *")
// URL validation
validator.ValidateURL("webhook-url", "https://example.com/webhook")
// Check for errors
if validator.HasErrors() {
for _, err := range validator.Errors() {
log.Printf("Validation error: %v", err)
}
}Sanitizer: config/sanitizer.go
Attack Vectors Protected:
| Attack Type | Protection Method | Blocked Patterns |
|---|---|---|
| SQL Injection | Pattern detection | union select, insert into, drop table, <script> |
| Shell Injection | Command validation | ; & | < >, &&, ||, $(), ` |
| Path Traversal | Path sanitization | ../, ..\\, %2e%2e, ~ |
| XSS | HTML escaping | <, >, &, ", ' |
| SSRF | URL validation + optional whitelist | Scheme validation, optional host whitelist |
| LDAP Injection | Character filtering | ( ) * | & ! |
Usage Examples:
sanitizer := config.NewSanitizer()
// String sanitization (removes control chars, null bytes)
clean, err := sanitizer.SanitizeString(userInput, 1024)
// Command validation
err := sanitizer.ValidateCommand("/backup/script.sh --dry-run")
// Path validation with base path restriction
err := sanitizer.ValidatePath("/var/log/backup.log", "/var/log")
// Docker image validation
err := sanitizer.ValidateDockerImage("nginx:1.21-alpine")
// Environment variable validation
err := sanitizer.ValidateEnvironmentVar("MY_VAR", "value123")
// URL validation (scheme and format)
err := sanitizer.ValidateURL("https://api.example.com/webhook")CommandValidator: config/command_validator.go
Features:
- Service name validation (Docker)
- File path validation (sensitive directories blocked)
- Command argument sanitization
- Dangerous pattern detection
- Null byte injection prevention
Blocked Patterns:
// Command substitution
$(...), `command`, ${var}
// Shell operators
; & | < > >> << && ||
// Directory traversal
../ ..\ %2e%2e
// Sensitive directories
/etc/, /proc/, /sys/, /dev/Example:
cmdValidator := config.NewCommandValidator()
// Validate service name
err := cmdValidator.ValidateServiceName("web-backend")
// Validate file path
err := cmdValidator.ValidateFilePath("/app/docker-compose.yml")
// Validate command arguments
args := []string{"--flag", "value", "/path/to/file"}
err := cmdValidator.ValidateCommandArgs(args)Resource Limits:
[job-run "isolated-job"]
image = myapp:latest
command = process-data
# Memory limits
memory = 512m
memory-swap = 1g
# CPU limits
cpu-shares = 512
cpu-quota = 50000Capabilities:
# Drop dangerous capabilities
capabilities-drop = NET_RAW,SYS_ADMIN,SYS_MODULE
# Add only required capabilities
capabilities-add = NET_BIND_SERVICEUser Restrictions:
# Run as non-root user
user = 1000:1000
# Or specific username
user = appuserNetwork Isolation:
# Isolated network
network = app_isolated
# No network access
network = noneDNS Restrictions:
# Internal DNS only
dns = 10.0.0.1,10.0.0.2Read-Only Mounts:
# Read-only data volume
volumes = /data:/data:ro
# Writable output only
volumes = /output:/output:rwTmpfs for Sensitive Data:
# Temporary in-memory storage
tmpfs = /tmp:rw,noexec,nosuid,size=100mImage Verification:
# Use SHA256 digests
image = nginx@sha256:abc123...
# Pull policy
pull = always # Always get latest security patchesTrusted Registries:
# Use only trusted registries
image = registry.example.com/myapp:1.0.0Socket Protection:
# Restrict socket access
chmod 660 /var/run/docker.sock
chown root:docker /var/run/docker.sock
# Or use TCP with TLS
DOCKER_HOST=tcp://docker:2376
DOCKER_TLS_VERIFY=1
DOCKER_CERT_PATH=/certsLocalJob Restrictions:
[global]
# Prevent LocalJobs from Docker labels
allow-host-jobs-from-labels = falseProduction Deployment:
# Behind reverse proxy (recommended)
services:
nginx:
image: nginx:alpine
ports:
- "443:443"
volumes:
- ./ssl:/etc/nginx/ssl:ro
- ./nginx.conf:/etc/nginx/nginx.conf:ro
ofelia:
image: netresearch/ofelia:latest
expose:
- "8080"
environment:
- OFELIA_WEB_ADDRESS=:8080NGINX Configuration:
server {
listen 443 ssl http2;
server_name ofelia.example.com;
ssl_certificate /etc/nginx/ssl/cert.pem;
ssl_certificate_key /etc/nginx/ssl/key.pem;
ssl_protocols TLSv1.2 TLSv1.3;
ssl_ciphers HIGH:!aNULL:!MD5;
location / {
proxy_pass http://ofelia:8080;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
}iptables Example:
# Allow only necessary ports
iptables -A INPUT -p tcp --dport 443 -j ACCEPT
iptables -A INPUT -p tcp --dport 8080 -j DROP # Block direct access
# Allow Docker network
iptables -A INPUT -i docker0 -j ACCEPT
# Rate limiting
iptables -A INPUT -p tcp --dport 443 -m state --state NEW \
-m recent --set --name WEB
iptables -A INPUT -p tcp --dport 443 -m state --state NEW \
-m recent --update --seconds 60 --hitcount 100 --name WEB -j DROPApplication-Level:
// HTTP rate limiting: 100 requests/minute per IP
rateLimiter := newRateLimiter(100, time.Minute)NGINX Rate Limiting:
limit_req_zone $binary_remote_addr zone=api:10m rate=10r/s;
location /api/ {
limit_req zone=api burst=20 nodelay;
proxy_pass http://ofelia:8080;
}Secret Management:
# Use secret management (Docker Swarm)
docker secret create web_secret_key web_secret_key.txt
docker secret create smtp_password smtp_password.txt
# Reference in compose file
services:
ofelia:
secrets:
- web_secret_key
- smtp_password
environment:
- OFELIA_WEB_SECRET_KEY_FILE=/run/secrets/web_secret_key
- OFELIA_SMTP_PASSWORD_FILE=/run/secrets/smtp_passwordKubernetes Secrets:
apiVersion: v1
kind: Secret
metadata:
name: ofelia-secrets
type: Opaque
data:
web-secret-key: <base64-encoded>
smtp-password: <base64-encoded>
---
apiVersion: apps/v1
kind: Deployment
spec:
template:
spec:
containers:
- name: ofelia
env:
- name: OFELIA_WEB_SECRET_KEY
valueFrom:
secretKeyRef:
name: ofelia-secrets
key: web-secret-keyDocker Compose:
services:
ofelia:
image: netresearch/ofelia:latest
user: "1000:1000" # Non-root user
cap_drop:
- ALL
cap_add:
- NET_BIND_SERVICE # Only if binding <1024
read_only: true
tmpfs:
- /tmp:noexec,nosuidContainer Scanning:
# Trivy
trivy image netresearch/ofelia:latest
# Clair
docker run -p 6060:6060 -d --name clair clair
clairctl analyze netresearch/ofelia:latestCode Scanning:
# Go vulnerability check
govulncheck ./...
# Static analysis
staticcheck ./...
gosec ./...- ✅ Use environment variables for all secrets
- ✅ Never commit secrets to version control
- ✅ Rotate credentials regularly (90 days)
- ✅ Use secret management systems (Vault, Secrets Manager)
- ✅ Minimum password length: 12 characters
- ✅ Enforce password complexity requirements
- ✅ Enable web authentication in production
- ✅ Use HTTPS/TLS for all external connections
- ✅ Implement IP whitelisting for API access
- ✅ Disable LocalJobs from Docker labels
- ✅ Use least privilege for container execution
- ✅ Regularly review and audit access logs
- ✅ Run containers as non-root users
- ✅ Drop unnecessary capabilities
- ✅ Use read-only root filesystems
- ✅ Implement resource limits (CPU, memory)
- ✅ Scan images for vulnerabilities
- ✅ Use official base images only
- ✅ Keep images updated (automated patches)
- ✅ Use isolated Docker networks
- ✅ Implement firewall rules
- ✅ Enable rate limiting
- ✅ Use reverse proxy with TLS
- ✅ Disable unnecessary ports
- ✅ Monitor network traffic
- ✅ Enable structured logging
- ✅ Monitor authentication failures
- ✅ Track failed job executions
- ✅ Set up alerting for security events
- ✅ Retain logs for 90 days minimum
- ✅ Implement centralized log aggregation
- ✅ Document incident response procedures
- ✅ Test backup and recovery regularly
- ✅ Maintain security contact information
- ✅ Have rollback procedures documented
- ✅ Conduct post-incident reviews
Recent security enhancements implemented:
- Secure token-based authentication wired up to web API
- Bcrypt password hashing (cost factor 12)
- Cryptographically secure token generation
- Configurable token expiry
- Auth middleware protects /api/* endpoints
- Dead auth code removed (legacy plain text auth, unused JWT handlers)
- Bcrypt hashing (cost factor 12)
- Constant-time username comparison
- Timing attack protection
- 100ms delay on authentication failure
- One-time use CSRF tokens
- Token validation middleware
- Secure cookie attributes
- SameSite cookie protection
- Per-IP rate limiting (5 attempts/minute for auth)
- HTTP rate limiting (100 requests/minute)
- Sliding window algorithm
- Automatic cleanup of old entries
- Docker image validation
- Command argument sanitization
- Environment variable validation
- Enhanced path traversal prevention
The web UI and API are disabled by default. If you enable them (enable-web = true), see Web Package Security for:
- Token authentication configuration
- Password hashing with bcrypt
- Rate limiting and CSRF protection
- Security headers
- Email: security@netresearch.de
- PGP Key: Available on keyserver
- Response Time: 48 hours for acknowledgment
- Send detailed vulnerability report to security contact
- Include: description, impact, reproduction steps, suggested fix
- Wait for acknowledgment (48 hours)
- Allow 90 days for patch development
- Coordinate disclosure timeline
Currently not available. Please report vulnerabilities responsibly.
- OWASP Top 10 (2021): Full coverage
- CWE Top 25: Mitigations implemented
- Docker CIS Benchmarks: Level 1 compliance
- NIST Cybersecurity Framework: Core functions addressed
All security-relevant events are logged:
- Authentication attempts (success/failure)
- Authorization decisions
- Configuration changes
- Job executions
- Container operations
- API requests
Log Retention: 90 days minimum recommended
- Web auth enabled with bcrypt password hash
- Secret key configured (or auto-generated)
- HTTPS/TLS enabled (via reverse proxy)
- All secrets in environment variables
- Container resource limits set
- Non-root user configured
- Unnecessary capabilities dropped
- Docker socket access restricted
- LocalJobs from labels disabled
- Rate limiting enabled
- Firewall rules configured
- Security scanning (Trivy/Clair)
- Vulnerability assessment
- Log monitoring enabled
- Alerting configured
- Backup tested
- Incident response plan documented
- Security training completed
- Access audit performed
- Monthly vulnerability scans
- Quarterly access reviews
- Regular credential rotation (90 days)
- Security patch updates (within 7 days for high severity)
- Log review (weekly minimum)
- Incident response drills (quarterly)
- ADR-002: Security Boundaries - Architectural decision on security responsibilities
- Web Package - Authentication and API security
- Config Package - Input validation and sanitization
- Middlewares Package - Middleware security
- Configuration Guide - Secure configuration practices
- PROJECT_INDEX - Overall system architecture
For security questions or vulnerability reports, contact: security@netresearch.de