Best practices for deploying Bengal sites securely. Since Bengal generates static files, most security concerns relate to HTTP headers, content policies, and build hygiene.
Security Headers
Configure your hosting platform to send security headers with every response.
Recommended Headers
| Header | Purpose |
|---|---|
Content-Security-Policy |
Prevents XSS and injection attacks |
X-Frame-Options |
Prevents clickjacking |
X-Content-Type-Options |
Prevents MIME-type sniffing |
Referrer-Policy |
Controls referrer information |
Permissions-Policy |
Restricts browser features |
Strict-Transport-Security |
Enforces HTTPS |
Platform Configuration
Createnetlify.tomlin your project root:
[[headers]]
for = "/*"
[headers.values]
# Prevent XSS attacks
Content-Security-Policy = """
default-src 'self';
script-src 'self' 'unsafe-inline';
style-src 'self' 'unsafe-inline';
img-src 'self' data: https:;
font-src 'self';
connect-src 'self';
frame-ancestors 'none';
"""
# Prevent clickjacking
X-Frame-Options = "DENY"
# Prevent MIME sniffing
X-Content-Type-Options = "nosniff"
# Control referrer
Referrer-Policy = "strict-origin-when-cross-origin"
# Restrict browser features
Permissions-Policy = "camera=(), microphone=(), geolocation=()"
Createvercel.jsonin your project root:
{
"headers": [
{
"source": "/(.*)",
"headers": [
{
"key": "Content-Security-Policy",
"value": "default-src 'self'; script-src 'self' 'unsafe-inline'; style-src 'self' 'unsafe-inline'; img-src 'self' data: https:; font-src 'self'; connect-src 'self'; frame-ancestors 'none';"
},
{
"key": "X-Frame-Options",
"value": "DENY"
},
{
"key": "X-Content-Type-Options",
"value": "nosniff"
},
{
"key": "Referrer-Policy",
"value": "strict-origin-when-cross-origin"
},
{
"key": "Permissions-Policy",
"value": "camera=(), microphone=(), geolocation=()"
}
]
}
]
}
Create_headers in your output directory (or configure via public/_headers):
/*
Content-Security-Policy: default-src 'self'; script-src 'self' 'unsafe-inline'; style-src 'self' 'unsafe-inline'; img-src 'self' data: https:; font-src 'self'; connect-src 'self'; frame-ancestors 'none';
X-Frame-Options: DENY
X-Content-Type-Options: nosniff
Referrer-Policy: strict-origin-when-cross-origin
Permissions-Policy: camera=(), microphone=(), geolocation=()
Add to your server block:
add_header Content-Security-Policy "default-src 'self'; script-src 'self' 'unsafe-inline'; style-src 'self' 'unsafe-inline'; img-src 'self' data: https:; font-src 'self'; connect-src 'self'; frame-ancestors 'none';" always;
add_header X-Frame-Options "DENY" always;
add_header X-Content-Type-Options "nosniff" always;
add_header Referrer-Policy "strict-origin-when-cross-origin" always;
add_header Permissions-Policy "camera=(), microphone=(), geolocation=()" always;
# HSTS (only if using HTTPS)
add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;
Add to.htaccessor server config:
<IfModule mod_headers.c>
Header always set Content-Security-Policy "default-src 'self'; script-src 'self' 'unsafe-inline'; style-src 'self' 'unsafe-inline'; img-src 'self' data: https:; font-src 'self'; connect-src 'self'; frame-ancestors 'none';"
Header always set X-Frame-Options "DENY"
Header always set X-Content-Type-Options "nosniff"
Header always set Referrer-Policy "strict-origin-when-cross-origin"
Header always set Permissions-Policy "camera=(), microphone=(), geolocation=()"
</IfModule>
Content Security Policy (CSP)
The Content-Security-Policy header is your primary defense against XSS attacks.
Base Policy
Start with a restrictive policy and add exceptions as needed:
default-src 'self';
script-src 'self';
style-src 'self';
img-src 'self';
font-src 'self';
connect-src 'self';
frame-ancestors 'none';
Common Additions
| Need | Add to CSP |
|---|---|
| Inline styles | style-src 'self' 'unsafe-inline' |
| Inline scripts | script-src 'self' 'unsafe-inline'(avoid if possible) |
| Google Fonts | font-src 'self' fonts.gstatic.com; style-src 'self' fonts.googleapis.com |
| Google Analytics | script-src 'self' www.googletagmanager.com; connect-src www.google-analytics.com |
| Plausible | script-src 'self' plausible.io; connect-src plausible.io |
| External images | img-src 'self' data: https: |
| YouTube embeds | frame-src www.youtube-nocookie.com www.youtube.com |
| Vimeo embeds | frame-src player.vimeo.com |
| CodePen embeds | frame-src codepen.io |
| CodeSandbox embeds | frame-src codesandbox.io |
| StackBlitz embeds | frame-src stackblitz.com |
| GitHub Gist | script-src 'self' gist.github.com |
| Asciinema | script-src 'self' asciinema.org |
Testing CSP
Use the browser console to identify CSP violations:
- Open DevTools (F12)
- Go to the Console tab
- Look for CSP violation errors
- Adjust your policy as needed
Build Hygiene
Exclude Sensitive Files from Output
Bengal provides multiple ways to exclude sensitive files from your build output:
Output format exclusions — Exclude pages from JSON, search, and LLM outputs:
# bengal.toml
[output_formats.options]
exclude_patterns = ["404.html", "search.html", "admin/*"]
exclude_sections = ["internal"]
Content layer exclusions — Exclude files during content discovery:
# config/_default/sources.yaml
sources:
- type: local
directory: content
exclude:
- "_drafts/*"
- "*.secret.md"
- "internal/*"
Dev server exclusions — Prevent file watching on sensitive paths:
# bengal.toml
[dev_server]
exclude_patterns = ["*.env", "secrets/*", "*.key"]
Best practice: Keep sensitive files outside your content directory entirely. Use .gitignoreto prevent them from being committed.
Verify Before Deploy
Run validation before deploying:
# Validate content and health
bengal validate
# Check for broken links
bengal health linkcheck
# Verify no drafts in production
grep -r "draft: true" content/ && echo "DRAFTS FOUND" || echo "No drafts in content"
CI/CD Validation
Add security checks to your CI pipeline:
# GitHub Actions example
- name: Security checks
run: |
# Validate no sensitive patterns in output
! grep -r "API_KEY\|SECRET\|PASSWORD" public/
# Validate no draft content in output
! grep -r "draft-banner\|data-draft" public/
# Run Bengal validation
bengal validate
Third-Party Scripts
Subresource Integrity (SRI)
When loading external scripts, use SRI to verify integrity:
<script
src="https://cdn.example.com/lib.js"
integrity="sha384-oqVuAfXRKap7fdgcCY5uykM6+R9GqQ8K/uxh..."
crossorigin="anonymous">
</script>
Generate SRI hashes:
# Using openssl
curl -s https://cdn.example.com/lib.js | openssl dgst -sha384 -binary | openssl base64 -A
# Using srihash.org
# Visit https://www.srihash.org/
Self-Host When Possible
Bengal's asset fingerprinting provides cache-busting without CDN dependencies:
# bengal.toml
fingerprint_assets = true # style.css → style.a1b2c3.css
Or in the assets section:
[assets]
fingerprint = true
minify = true
optimize = true
Benefits of self-hosting:
- No third-party dependencies
- No tracking via CDN
- Full control over updates
- Works offline
HTTPS Enforcement
Verify baseurl
Ensure your configuration uses HTTPS:
[site]
baseurl = "https://example.com" # ✅ Use https://
# baseurl = "http://example.com" # ❌ Avoid http://
HSTS Header
Enable HTTP Strict Transport Security:
Strict-Transport-Security: max-age=31536000; includeSubDomains
Warning
Only enable HSTS after confirming HTTPS works correctly. HSTS is cached by browsers and difficult to undo.
Media Embed Security
Bengal's media directives ({youtube}, {vimeo}, {gist}, etc.) include built-in security protections:
Input Validation
All embed IDs and paths are validated via strict regex patterns:
| Directive | Validation |
|---|---|
{youtube} |
Exactly 11 characters: letters, numbers, underscores, hyphens |
{vimeo} |
6-11 digit numeric ID |
{gist} |
username/32-character-hex-idformat |
{figure} |
Safe paths starting with/ or ./, no ../traversal |
{video} |
Extensions:.mp4, .webm, .ogg, .mov |
{audio} |
Extensions:.mp3, .ogg, .wav, .flac, .m4a, .aac |
Malicious inputs are rejected with clear error messages:
:::{youtube} <script>alert(1)</script>
:title: Test
:::
Renders as an error block, not an embed.
Privacy by Default
External embeds use privacy-respecting modes:
- YouTube: Uses
youtube-nocookie.comby default - Vimeo: Enables Do Not Track (
dnt=1) by default
Iframe Sandboxing
External iframes include restricted sandbox attributes to limit capabilities.
CSP for Media Embeds
If using media embeds, add these frame sources to your CSP:
frame-src 'self' www.youtube-nocookie.com www.youtube.com player.vimeo.com codepen.io codesandbox.io stackblitz.com;
script-src 'self' gist.github.com asciinema.org;
Or selectively based on which directives you use.
Dependency Security
Audit Python Dependencies
# Using pip-audit
pip install pip-audit
pip-audit
# Using uv
uv pip audit
# Using safety
pip install safety
safety check
Keep Dependencies Updated
# Check for outdated packages
pip list --outdated
# Update Bengal
pip install --upgrade bengal
Draft Protection
Drafts are automatically excluded from listings, sitemap, search index, and RSS feeds.
How Drafts Work
Mark pages as drafts in frontmatter:
---
title: Work in Progress
draft: true
---
Draft pages are automatically excluded from:
site.pagesqueries (listings)- Sitemap generation
- Search index
- RSS feeds
Verify Output
# Build for production
bengal build
# Search for draft indicators in output
grep -r "draft-banner" public/ # Should return nothing
grep -r "data-draft" public/ # Should return nothing
# Verify drafts not in search index
! grep -q '"draft":true' public/index.json
CI Check
- name: Verify no drafts
run: |
bengal build
if grep -rq "draft-banner\|data-draft" public/; then
echo "ERROR: Draft content found in production build"
exit 1
fi
Security Checklist
Before deploying to production:
Headers
- Content-Security-Policy configured
- X-Frame-Options: DENY
- X-Content-Type-Options: nosniff
- Referrer-Policy set
- HSTS enabled (if HTTPS confirmed working)
Build
- Sensitive files excluded from content directory
- No API keys or secrets in content
- Drafts marked in frontmatter (auto-excluded from output)
- Validation passes (
bengal validate) - Links checked (
bengal health linkcheck)
Dependencies
- Dependencies audited for vulnerabilities
- Bengal and dependencies up to date
- SRI used for external scripts
Configuration
- baseurl uses HTTPS
- No debug settings in production config
- Environment-specific configs reviewed
Reporting Security Issues
Found a security vulnerability in Bengal?
Do NOT open a public GitHub issue.
Email security concerns to: lbeezr@icloud.com
We'll respond within 48 hours and work with you on a fix before public disclosure.
Seealso
- Deployment Guide
- CI/CD Setup
- Configuration Reference
- Mozilla Observatory — Test your site's security headers