Skip to main content

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)
  • /userContent uploads

This causes two issues:

  1. 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.

  2. CSP sandbox without allow-same-origin breaks authentication: When using sandbox directive without allow-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-origin with sandbox reduces sandboxing effectiveness
  • ⚠️ Dangerous if untrusted users can trigger builds or modify workspace files
  • ⚠️ Not recommended by Jenkins security team for production

Implementation: Serve artifacts from a separate domain without CSP restrictions.

How it works:

  1. Configure second domain: jenkins-files-ci-bfhweb.internal.quatico.dev
  2. Jenkins generates special URLs with security tokens
  3. Artifacts served via this URL bypass CSP
  4. 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 (sandbox restrictions 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:80
  • jenkins-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​

  1. Navigate to: Manage Jenkins → System
  2. Find: Resource Root URL
  3. Set: https://jenkins-files-ci-bfhweb.internal.quatico.dev
  4. Save

Step 4: How Jenkins Uses Resource Root URL​

When Resource Root URL is configured:

  1. User accesses artifact in Jenkins UI
  2. Jenkins generates special URL:
    https://jenkins-files-ci-bfhweb.internal.quatico.dev/<TOKEN>/artifact/.../report.html
  3. Browser requests from jenkins-files domain
  4. Nginx routes to jenkins-files-service
  5. Service routes to Jenkins pod (same pod!)
  6. Jenkins validates token
  7. Jenkins serves file WITHOUT CSP headers
  8. JavaScript executes successfully

Step 5: Verification​

After configuration:

  1. Trigger a design test build
  2. Check artifacts are accessible via Resource Root URL
  3. Verify HTML report JavaScript executes
  4. 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-bfhweb as separate origin from jenkins-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:

  1. Phase 1 (Current): Use CSP relaxation for development/testing
  2. Phase 2 (Before Production): Implement Resource Root URL
  3. Phase 3: Remove CSP relaxation, rely on Resource Root URL

Migration Checklist​

  • Create jenkins-files-service in 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​

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-scripts without allow-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:

  1. Is jenkins-files-service deployed? kubectl get svc -n bfhweb-ci
  2. Does service selector match Jenkins pod? Check labels
  3. Is nginx routing configured correctly? Check ingress logs
  4. 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