diff --git a/app/controllers/api/forward_auth_controller.rb b/app/controllers/api/forward_auth_controller.rb index 2b722df..5a06ea2 100644 --- a/app/controllers/api/forward_auth_controller.rb +++ b/app/controllers/api/forward_auth_controller.rb @@ -135,9 +135,6 @@ module Api def render_unauthorized(reason = nil) Rails.logger.info "ForwardAuth: Unauthorized - #{reason}" - # Set header to help with debugging - response.headers["X-Auth-Reason"] = reason if reason - # Get the redirect URL from query params or construct default redirect_url = validate_redirect_url(params[:rd]) base_url = redirect_url || "https://clinch.aapamilne.com" @@ -179,9 +176,6 @@ module Api def render_forbidden(reason = nil) Rails.logger.info "ForwardAuth: Forbidden - #{reason}" - # Set header to help with debugging - response.headers["X-Auth-Reason"] = reason if reason - # Return 403 Forbidden head :forbidden end diff --git a/config/environments/production.rb b/config/environments/production.rb index 6211472..af3d06b 100644 --- a/config/environments/production.rb +++ b/config/environments/production.rb @@ -81,11 +81,37 @@ Rails.application.configure do config.active_record.attributes_for_inspect = [ :id ] # Enable DNS rebinding protection and other `Host` header attacks. - # config.hosts = [ - # "example.com", # Allow requests from example.com - # /.*\.example\.com/ # Allow requests from subdomains like `www.example.com` - # ] - # + # Configure allowed hosts based on deployment scenario + allowed_hosts = [ + ENV.fetch('CLINCH_HOST', 'auth.aapamilne.com'), # External domain + /.*#{ENV.fetch('CLINCH_HOST', 'aapamilne\.com').gsub('.', '\.')}/ # Subdomains + ] + + # Allow Docker service names if running in same compose + if ENV['CLINCH_DOCKER_SERVICE_NAME'] + allowed_hosts << ENV['CLINCH_DOCKER_SERVICE_NAME'] + end + + # Allow internal IP access for cross-compose or host networking + if ENV['CLINCH_ALLOW_INTERNAL_IPS'] == 'true' + # Specific host IP + allowed_hosts << '192.168.2.246' + + # Private IP ranges for internal network access + allowed_hosts += [ + /192\.168\.\d+\.\d+/, # 192.168.0.0/16 private network + /10\.\d+\.\d+\.\d+/, # 10.0.0.0/8 private network + /172\.(1[6-9]|2[0-9]|3[0-1])\.\d+\.\d+/ # 172.16.0.0/12 private network + ] + end + + # Local development fallbacks + if ENV['CLINCH_ALLOW_LOCALHOST'] == 'true' + allowed_hosts += ['localhost', '127.0.0.1', '0.0.0.0'] + end + + config.hosts = allowed_hosts + # Skip DNS rebinding protection for the default health check endpoint. - # config.host_authorization = { exclude: ->(request) { request.path == "/up" } } + config.host_authorization = { exclude: ->(request) { request.path == "/up" } } end diff --git a/config/initializers/content_security_policy.rb b/config/initializers/content_security_policy.rb index d51d713..10b1ccc 100644 --- a/config/initializers/content_security_policy.rb +++ b/config/initializers/content_security_policy.rb @@ -4,26 +4,74 @@ # See the Securing Rails Applications Guide for more information: # https://guides.rubyonrails.org/security.html#content-security-policy-header -# Rails.application.configure do -# config.content_security_policy do |policy| -# policy.default_src :self, :https -# policy.font_src :self, :https, :data -# policy.img_src :self, :https, :data -# policy.object_src :none -# policy.script_src :self, :https -# policy.style_src :self, :https -# # Specify URI for violation reports -# # policy.report_uri "/csp-violation-report-endpoint" -# end -# -# # Generate session nonces for permitted importmap, inline scripts, and inline styles. -# config.content_security_policy_nonce_generator = ->(request) { request.session.id.to_s } -# config.content_security_policy_nonce_directives = %w(script-src style-src) -# -# # Automatically add `nonce` to `javascript_tag`, `javascript_include_tag`, and `stylesheet_link_tag` -# # if the corresponding directives are specified in `content_security_policy_nonce_directives`. -# # config.content_security_policy_nonce_auto = true -# -# # Report violations without enforcing the policy. -# # config.content_security_policy_report_only = true -# end +Rails.application.configure do + config.content_security_policy do |policy| + # Default policy: only allow resources from same origin and HTTPS + policy.default_src :self, :https + + # Scripts: strict security with nonce support for dynamic content + policy.script_src :self, :https, :strict_dynamic + + # Styles: allow inline styles for CSS frameworks, but require HTTPS + policy.style_src :self, :https, :unsafe_inline + + # Images: allow data URIs for inline images and HTTPS sources + policy.img_src :self, :https, :data + + # Fonts: allow self-hosted and HTTPS fonts, plus data URIs + policy.font_src :self, :https, :data + + # Media: allow self and HTTPS media sources + policy.media_src :self, :https + + # Objects: block potentially dangerous plugins + policy.object_src :none + + # Base URI: restrict base tag to same origin + policy.base_uri :self + + # Form actions: only allow forms to submit to same origin + policy.form_action :self + + # Frame ancestors: prevent clickjacking by disallowing framing + policy.frame_ancestors :none + + # Frame sources: block iframes unless explicitly needed + policy.frame_src :none + + # Connect sources: control where XHR/Fetch can connect + policy.connect_src :self, :https + + # Manifest: only allow same-origin manifest files + policy.manifest_src :self + + # Worker sources: control web worker origins + policy.worker_src :self, :https + + # Report URI: send violation reports to our monitoring endpoint + if Rails.env.production? + policy.report_uri "/api/csp-violation-report" + end + end + + # Generate session nonces for permitted inline scripts and styles + config.content_security_policy_nonce_generator = ->(request) { + # Use a secure random nonce instead of session ID for better security + SecureRandom.base64(16) + } + + # Apply nonces to script and style directives + config.content_security_policy_nonce_directives = %w(script-src style-src) + + # Automatically add `nonce` attributes to script/style tags + config.content_security_policy_nonce_auto = true + + # Enforce CSP in production, but use report-only in development for debugging + if Rails.env.production? + # Enforce the policy in production + config.content_security_policy_report_only = false + else + # Report violations only in development (helps with debugging) + config.content_security_policy_report_only = true + end +end diff --git a/config/routes.rb b/config/routes.rb index 8decd97..71d05d1 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -31,6 +31,7 @@ Rails.application.routes.draw do # ForwardAuth / Trusted Header SSO namespace :api do get "/verify", to: "forward_auth#verify" + post "/csp-violation-report", to: "csp#violation_report" end # Authenticated routes diff --git a/docs/forward-auth.md b/docs/forward-auth.md index 5939ad8..907cfe9 100644 --- a/docs/forward-auth.md +++ b/docs/forward-auth.md @@ -193,17 +193,179 @@ curl -v http://localhost:9000/api/verify?rd=https://clinch.example.com # Or 200 OK if you have a valid session cookie ``` +## Security Considerations + +### Content Security Policy (CSP) + +Clinch includes a comprehensive Content Security Policy to prevent Cross-Site Scripting (XSS) attacks by controlling which resources can be loaded by the browser. + +**What CSP Prevents:** +- Malicious script injection attacks +- Unauthorized resource loading +- Clickjacking through iframe protection +- Data exfiltration through unauthorized connections + +**CSP Features:** +- **Strict script control**: Only allows scripts from same origin or HTTPS +- **Nonce support**: Allows specific inline scripts with cryptographic nonces +- **Frame protection**: Prevents clickjacking attacks +- **Resource restrictions**: Controls images, fonts, styles, and media sources +- **Violation reporting**: Monitors and logs attempted XSS attacks + +**Development vs Production:** +- **Development**: Report-only mode for debugging CSP violations +- **Production**: Full enforcement with violation logging + +### DNS Rebinding Protection + +Clinch includes built-in DNS rebinding protection for enhanced security in all deployment scenarios. + +**What is DNS Rebinding?** +DNS rebinding attacks trick a victim's browser into accessing internal network resources by manipulating DNS responses, potentially allowing attackers to probe your authentication system. + +**Clinch's Protection Layers:** +1. **Rails Host Validation**: Blocks unauthorized domains at the application level +2. **Infrastructure Security**: Caddy/Reverse proxy provides additional protection +3. **Environment-Specific Configuration**: Adapts to your deployment scenario + +### Deployment Scenarios + +#### Scenario 1: Same Docker Compose (Recommended) +```yaml +# docker-compose.yml +services: + caddy: + # ... caddy configuration + + clinch: + image: reg.tbdb.info/clinch:latest + environment: + - CLINCH_HOST=auth.aapamilne.com + - CLINCH_DOCKER_SERVICE_NAME=clinch # Enable service name access + - CLINCH_ALLOW_INTERNAL_IPS=true # Allow backup IP access + - CLINCH_ALLOW_LOCALHOST=false +``` + +**Caddy Configuration:** +```caddyfile +metube.aapamilne.com { + forward_auth clinch:3000 { # Docker service name (preferred) + uri /api/verify + copy_headers Remote-User Remote-Email Remote-Groups Remote-Admin + } + + handle { + reverse_proxy * { + to http://192.168.2.223:8081 + } + } +} +``` + +**Security Benefits:** +- ✅ Docker network isolation prevents external access +- ✅ Service names resolve to unpredictable internal IPs +- ✅ Natural DNS rebinding protection +- ✅ Application-level host validation as backup + +#### Scenario 2: Separate Docker Composes (Current Setup) +```yaml +# clinch-compose/.env +CLINCH_HOST=auth.aapamilne.com +CLINCH_ALLOW_INTERNAL_IPS=true +CLINCH_ALLOW_LOCALHOST=false +CLINCH_DOCKER_SERVICE_NAME= +``` + +**Caddy Configuration:** +```caddyfile +metube.aapamilne.com { + forward_auth 192.168.2.246:3000 { # IP access across composes + uri /api/verify + copy_headers Remote-User Remote-Email Remote-Groups Remote-Admin + } +} +``` + +**Security Benefits:** +- ✅ Rails host validation blocks unauthorized domains +- ✅ Only allows private IP ranges and your domain +- ✅ Defense in depth (application + infrastructure security) + +#### Scenario 3: External Deployment +```yaml +# Production environment +environment: + - CLINCH_HOST=auth.example.com + - CLINCH_ALLOW_INTERNAL_IPS=false # Stricter for external + - CLINCH_ALLOW_LOCALHOST=false +``` + +**Caddy Configuration:** +```caddyfile +app.example.com { + forward_auth auth.example.com:3000 { # External domain only + uri /api/verify + copy_headers Remote-User Remote-Email Remote-Groups Remote-Admin + } +} +``` + +**Security Benefits:** +- ✅ Only allows your external domain +- ✅ Blocks internal IP access +- ✅ Maximum security for public deployments + +### Host Validation Environment Variables + +| Variable | Default | Purpose | Recommended Setting | +|----------|---------|---------|-------------------| +| `CLINCH_HOST` | `auth.aapamilne.com` | Primary domain | Always set to your auth domain | +| `CLINCH_DOCKER_SERVICE_NAME` | `nil` | Docker service name | Set to service name in same compose | +| `CLINCH_ALLOW_INTERNAL_IPS` | `true` | Allow private IPs | `true` for internal, `false` for external | +| `CLINCH_ALLOW_LOCALHOST` | `false` | Allow localhost access | `true` for development only | + +### Security Architecture + +Clinch provides **defense in depth** security with multiple protection layers: + +**Application-Level Security:** +- Host validation prevents unauthorized domain access +- Session-based authentication with secure cookies +- Rate limiting on sensitive endpoints +- Input validation and sanitization +- Content Security Policy (CSP) prevents XSS attacks + +**Infrastructure Security:** +- Docker network isolation +- Reverse proxy access control +- SSL/TLS encryption +- Private network restrictions + +**Benefits of Multi-Layer Security:** +- If infrastructure security fails, application security still protects +- Flexible deployment options without compromising security +- Environment-specific configuration for different threat models + ## Troubleshooting ### Common Issues 1. **Authentication Loop**: Check that cookies are set on the root domain 2. **Session Not Shared**: Verify `extract_root_domain` is working correctly -3. **Caddy Connection**: Ensure `clinch:9000` resolves from your Caddy container +3. **Caddy Connection**: Ensure service name/IP resolves from your Caddy container 4. **Race Condition After Authentication**: - **Problem**: Forward auth fails immediately after login due to cookie timing - **Solution**: One-time tokens automatically bridge this gap - **Debug**: Look for "ForwardAuth: Valid one-time token used" in logs +5. **Host Validation Errors**: + - **Problem**: "Blocked host: [host]" errors in logs + - **Solution**: Check `CLINCH_HOST` and other environment variables + - **Debug**: Verify your Caddy configuration matches allowed hosts +6. **DNS Rebinding Protection**: + - **Problem**: Legitimate requests blocked as "unauthorized host" + - **Solution**: Ensure your deployment scenario matches environment variables + - **Debug**: Check Rails logs for host validation messages ### Debug Logging