Jenkins Resource Root URL for Visual Regression Reports
Status: Stable
Overview
Jenkins Resource Root URL is a security feature introduced in Jenkins 2.200+ that provides a safer alternative to relaxing Content Security Policy (CSP) for serving HTML reports with JavaScript.
The Problem
By default, Jenkins applies a strict CSP to all files served via DirectoryBrowserSupport, which includes:
- Workspace files
- Archived artifacts (including visual regression reports)
/userContentuploads
This causes two issues:
-
CSP blocks inline JavaScript: The strict CSP prevents inline JavaScript execution to prevent XSS attacks from malicious files. However, legitimate HTML reports (like reg-cli visual regression reports) need inline JavaScript to function.
-
CSP
sandboxwithoutallow-same-originbreaks authentication: When usingsandboxdirective withoutallow-same-origin, the document is treated as having a unique origin. This prevents cookies and Referer headers from being sent with same-origin requests, causing authentication to fail for images and other subresources.
Solution Options
Option A: Relax Jenkins CSP (MVP Approach)
Implementation: Set Jenkins system property via Script Console or startup args:
System.setProperty("hudson.model.DirectoryBrowserSupport.CSP",
"sandbox allow-scripts allow-same-origin; " +
"style-src 'self' 'unsafe-inline' https://fonts.googleapis.com; " +
"script-src 'self' 'unsafe-inline'; " +
"img-src * data: blob:; " +
"font-src 'self' https://fonts.gstatic.com data:;")
Critical: The allow-same-origin directive is required for cookies and Referer headers to be sent with image requests. Without it, images fail to load due to authentication redirects.
Pros:
- ✅ Simple configuration
- ✅ Works immediately
- ✅ No infrastructure changes
Cons:
- ⚠️ Security risk: Allows inline scripts in ALL served files (workspace, artifacts, etc.)
- ⚠️
allow-same-originwithsandboxreduces sandboxing effectiveness - ⚠️ Dangerous if untrusted users can trigger builds or modify workspace files
- ⚠️ Not recommended by Jenkins security team for production
Option B: Resource Root URL (Recommended for Production)
Implementation: Serve artifacts from a separate domain without CSP restrictions.
How it works:
- Configure second domain:
jenkins-files-ci-bfhweb.internal.quatico.dev - Jenkins generates special URLs with security tokens
- Artifacts served via this URL bypass CSP
- Browser treats it as separate origin (more secure isolation)
Pros:
- ✅ More secure - isolates potentially untrusted content from main Jenkins origin
- ✅ Officially recommended by Jenkins security team
- ✅ No global CSP relaxation needed (
sandboxrestrictions don't apply) - ✅ Token-based access control (no session cookies required)
- ✅ Works in all browsers without requiring
allow-same-origin
Cons:
- ⚠️ Requires infrastructure setup (additional domain/service)
- ⚠️ More complex configuration
Resource Root URL Implementation Guide
Prerequisites
- Jenkins 2.200 or later
- Kubernetes cluster with nginx ingress proxy
- Understanding of K8s services and routing
Step 1: Create Kubernetes Service
Create a second service pointing to the same Jenkins pod:
# deployments/ci/jenkins/service-files.yaml
apiVersion: v1
kind: Service
metadata:
name: jenkins-files-service
namespace: bfhweb-ci
spec:
selector:
app.kubernetes.io/name: jenkins # Same selector as jenkins-service!
ports:
- name: http
port: 80
protocol: TCP
targetPort: 8080
type: ClusterIP
Key insight: This service routes to the same Jenkins pod as the main service. Jenkins changes behavior based on the hostname/token in requests.
Step 2: Nginx Ingress Routing (Already Configured)
The nginx ingress proxy already has wildcard routing configured:
# Pattern from nginx.conf line 70:
~^(?<service>.*)-(?<environment>[^-]*)-(?<project>[^-]*).internal.quatico.dev$
→ $service-service.$project-$environment.svc.cluster.local:80
This means:
jenkins-ci-apps.internal.quatico.dev/job/bfhweb→jenkins-service.bfhweb-ci.svc.cluster.local:80jenkins-files-ci-bfhweb.internal.quatico.dev→jenkins-files-service.bfhweb-ci.svc.cluster.local:80
No additional nginx configuration needed!
Step 3: Configure Jenkins Resource Root URL
- Navigate to: Manage Jenkins → System
- Find: Resource Root URL
- Set:
https://jenkins-files-ci-bfhweb.internal.quatico.dev - Save
Step 4: How Jenkins Uses Resource Root URL
When Resource Root URL is configured:
- User accesses artifact in Jenkins UI
- Jenkins generates special URL:
https://jenkins-files-ci-bfhweb.internal.quatico.dev/<TOKEN>/artifact/.../report.html - Browser requests from jenkins-files domain
- Nginx routes to
jenkins-files-service - Service routes to Jenkins pod (same pod!)
- Jenkins validates token
- Jenkins serves file WITHOUT CSP headers
- JavaScript executes successfully
Step 5: Verification
After configuration:
- Trigger a design test build
- Check artifacts are accessible via Resource Root URL
- Verify HTML report JavaScript executes
- Confirm images load correctly
Security Considerations
Resource Root URL Security Model
How tokens protect access:
- URLs include cryptographic tokens encoding:
- File path
- User who generated the URL
- Timestamp (when URL was created)
- Token validation prevents unauthorized access
- URLs expire based on session/configuration
Domain isolation benefits:
- Browser treats
jenkins-files-ci-bfhwebas separate origin fromjenkins-ci-apps/job/bfhweb - Scripts at Resource Root URL cannot access Jenkins session/cookies
- Limits impact of malicious HTML/JS files
Remaining risks:
- User who can archive malicious HTML can still execute JS in viewer's browser
- But limited to Resource Root URL context, not Jenkins context
- Much safer than global CSP relaxation
When to Use Each Option
Use CSP Relaxation (Option A) when:
- ✅ All users are fully trusted
- ✅ All build agents are trusted
- ✅ No external PR builds
- ✅ MVP/prototype phase
- ✅ Low-risk internal environment
Use Resource Root URL (Option B) when:
- ✅ Production environment
- ✅ Untrusted users can trigger builds
- ✅ External PR builds enabled
- ✅ Compliance/security requirements
- ✅ Best practices needed
Migration Path: CSP → Resource Root URL
For the BFH MVP:
- Phase 1 (Current): Use CSP relaxation for development/testing
- Phase 2 (Before Production): Implement Resource Root URL
- Phase 3: Remove CSP relaxation, rely on Resource Root URL
Migration Checklist
- Create
jenkins-files-servicein K8s - Apply service to cluster:
kubectl apply -f service-files.yaml - Configure Jenkins Resource Root URL setting
- Test artifact access via Resource Root URL
- Verify HTML reports work correctly
- Update documentation with production URLs
- Remove/revert CSP relaxation setting
- Update CLAUDE.md with production configuration
References
- Jenkins Security: Configuring Content Security Policy
- Jenkins Security: Serving User Content
- Badge Plugin
- Nginx Ingress Proxy Configuration
Troubleshooting
Images fail to load - Authentication redirects (Missing allow-same-origin)
Symptom:
- Report accessed via artifact URL loads but images show as broken/blank with loading spinners
- Network requests for images return 302 redirects to Keycloak authentication
- Images may work in Safari but fail in Chrome
- Navigating directly to image URL in new tab works correctly
Root Cause:
- CSP uses
sandbox allow-scriptswithoutallow-same-origin - This treats the document as having a unique origin
- Cookies and Referer headers are NOT sent with same-origin image requests
- Without authentication cookies, requests redirect to Keycloak auth
- Images cannot follow OAuth redirects (browsers don't allow this)
Solution:
Add allow-same-origin to the CSP sandbox directive:
System.setProperty("hudson.model.DirectoryBrowserSupport.CSP",
"sandbox allow-scripts allow-same-origin; " +
"style-src 'self' 'unsafe-inline' https://fonts.googleapis.com; " +
"script-src 'self' 'unsafe-inline'; " +
"img-src * data: blob:; " +
"font-src 'self' https://fonts.gstatic.com data:;")
Verification: After applying the fix, check network requests:
- Images should return 200 (not 302)
- Request headers should include
referer:(not empty) - Response should be actual image data (not auth redirect)
Reference:
Resource Root URL returns 404
Check:
- Is
jenkins-files-servicedeployed?kubectl get svc -n bfhweb-ci - Does service selector match Jenkins pod? Check labels
- Is nginx routing configured correctly? Check ingress logs
- Can you curl the service directly from within cluster?
Tokens expire too quickly
Jenkins tokens include session information. Check:
- Session timeout settings
- Jenkins system clock vs. client clock
- Token generation configuration
Future Enhancements
- Automated service deployment via Helm chart
- Monitoring/alerting for Resource Root URL availability
- Documentation for other projects (MCHWEB, EWZ)
- Integration with ArgoCD for GitOps deployment