Add comprhensive csp polices and reporting endpoint. Add environment support require for protecting against rebinding attacks on ip addresses
This commit is contained in:
@@ -135,9 +135,6 @@ module Api
|
|||||||
def render_unauthorized(reason = nil)
|
def render_unauthorized(reason = nil)
|
||||||
Rails.logger.info "ForwardAuth: Unauthorized - #{reason}"
|
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
|
# Get the redirect URL from query params or construct default
|
||||||
redirect_url = validate_redirect_url(params[:rd])
|
redirect_url = validate_redirect_url(params[:rd])
|
||||||
base_url = redirect_url || "https://clinch.aapamilne.com"
|
base_url = redirect_url || "https://clinch.aapamilne.com"
|
||||||
@@ -179,9 +176,6 @@ module Api
|
|||||||
def render_forbidden(reason = nil)
|
def render_forbidden(reason = nil)
|
||||||
Rails.logger.info "ForwardAuth: Forbidden - #{reason}"
|
Rails.logger.info "ForwardAuth: Forbidden - #{reason}"
|
||||||
|
|
||||||
# Set header to help with debugging
|
|
||||||
response.headers["X-Auth-Reason"] = reason if reason
|
|
||||||
|
|
||||||
# Return 403 Forbidden
|
# Return 403 Forbidden
|
||||||
head :forbidden
|
head :forbidden
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -81,11 +81,37 @@ Rails.application.configure do
|
|||||||
config.active_record.attributes_for_inspect = [ :id ]
|
config.active_record.attributes_for_inspect = [ :id ]
|
||||||
|
|
||||||
# Enable DNS rebinding protection and other `Host` header attacks.
|
# Enable DNS rebinding protection and other `Host` header attacks.
|
||||||
# config.hosts = [
|
# Configure allowed hosts based on deployment scenario
|
||||||
# "example.com", # Allow requests from example.com
|
allowed_hosts = [
|
||||||
# /.*\.example\.com/ # Allow requests from subdomains like `www.example.com`
|
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.
|
# 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
|
end
|
||||||
|
|||||||
@@ -4,26 +4,74 @@
|
|||||||
# See the Securing Rails Applications Guide for more information:
|
# See the Securing Rails Applications Guide for more information:
|
||||||
# https://guides.rubyonrails.org/security.html#content-security-policy-header
|
# https://guides.rubyonrails.org/security.html#content-security-policy-header
|
||||||
|
|
||||||
# Rails.application.configure do
|
Rails.application.configure do
|
||||||
# config.content_security_policy do |policy|
|
config.content_security_policy do |policy|
|
||||||
# policy.default_src :self, :https
|
# Default policy: only allow resources from same origin and HTTPS
|
||||||
# policy.font_src :self, :https, :data
|
policy.default_src :self, :https
|
||||||
# policy.img_src :self, :https, :data
|
|
||||||
# policy.object_src :none
|
# Scripts: strict security with nonce support for dynamic content
|
||||||
# policy.script_src :self, :https
|
policy.script_src :self, :https, :strict_dynamic
|
||||||
# policy.style_src :self, :https
|
|
||||||
# # Specify URI for violation reports
|
# Styles: allow inline styles for CSS frameworks, but require HTTPS
|
||||||
# # policy.report_uri "/csp-violation-report-endpoint"
|
policy.style_src :self, :https, :unsafe_inline
|
||||||
# end
|
|
||||||
#
|
# Images: allow data URIs for inline images and HTTPS sources
|
||||||
# # Generate session nonces for permitted importmap, inline scripts, and inline styles.
|
policy.img_src :self, :https, :data
|
||||||
# config.content_security_policy_nonce_generator = ->(request) { request.session.id.to_s }
|
|
||||||
# config.content_security_policy_nonce_directives = %w(script-src style-src)
|
# Fonts: allow self-hosted and HTTPS fonts, plus data URIs
|
||||||
#
|
policy.font_src :self, :https, :data
|
||||||
# # 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`.
|
# Media: allow self and HTTPS media sources
|
||||||
# # config.content_security_policy_nonce_auto = true
|
policy.media_src :self, :https
|
||||||
#
|
|
||||||
# # Report violations without enforcing the policy.
|
# Objects: block potentially dangerous plugins
|
||||||
# # config.content_security_policy_report_only = true
|
policy.object_src :none
|
||||||
# end
|
|
||||||
|
# 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
|
||||||
|
|||||||
@@ -31,6 +31,7 @@ Rails.application.routes.draw do
|
|||||||
# ForwardAuth / Trusted Header SSO
|
# ForwardAuth / Trusted Header SSO
|
||||||
namespace :api do
|
namespace :api do
|
||||||
get "/verify", to: "forward_auth#verify"
|
get "/verify", to: "forward_auth#verify"
|
||||||
|
post "/csp-violation-report", to: "csp#violation_report"
|
||||||
end
|
end
|
||||||
|
|
||||||
# Authenticated routes
|
# Authenticated routes
|
||||||
|
|||||||
@@ -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
|
# 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
|
## Troubleshooting
|
||||||
|
|
||||||
### Common Issues
|
### Common Issues
|
||||||
|
|
||||||
1. **Authentication Loop**: Check that cookies are set on the root domain
|
1. **Authentication Loop**: Check that cookies are set on the root domain
|
||||||
2. **Session Not Shared**: Verify `extract_root_domain` is working correctly
|
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**:
|
4. **Race Condition After Authentication**:
|
||||||
- **Problem**: Forward auth fails immediately after login due to cookie timing
|
- **Problem**: Forward auth fails immediately after login due to cookie timing
|
||||||
- **Solution**: One-time tokens automatically bridge this gap
|
- **Solution**: One-time tokens automatically bridge this gap
|
||||||
- **Debug**: Look for "ForwardAuth: Valid one-time token used" in logs
|
- **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
|
### Debug Logging
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user