diff --git a/.github/workflows/deploy-worker.yml b/.github/workflows/deploy-worker.yml
new file mode 100644
index 0000000..b7af9d0
--- /dev/null
+++ b/.github/workflows/deploy-worker.yml
@@ -0,0 +1,31 @@
+name: Deploy Cloudflare Worker (security headers)
+
+on:
+ push:
+ branches:
+ - main
+ paths:
+ - "cloudflare/**"
+ workflow_dispatch:
+
+jobs:
+ deploy:
+ name: Deploy worker
+ runs-on: ubuntu-latest
+ permissions:
+ contents: read
+ defaults:
+ run:
+ working-directory: cloudflare
+
+ steps:
+ # actions/checkout@v4
+ - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683
+
+ # cloudflare/wrangler-action@v3
+ - name: Deploy with Wrangler
+ uses: cloudflare/wrangler-action@da0e0dfe58b7a431659754fdf3f186c529afbe65
+ with:
+ apiToken: ${{ secrets.CLOUDFLARE_API_TOKEN }}
+ accountId: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
+ workingDirectory: cloudflare
diff --git a/_layouts/default.html b/_layouts/default.html
index 7f5ed97..592fa63 100644
--- a/_layouts/default.html
+++ b/_layouts/default.html
@@ -14,9 +14,6 @@
-
-
-
diff --git a/cloudflare/worker.js b/cloudflare/worker.js
new file mode 100644
index 0000000..3a992d5
--- /dev/null
+++ b/cloudflare/worker.js
@@ -0,0 +1,59 @@
+/**
+ * Cloudflare Worker – adds HTTP security headers to every response.
+ *
+ * Deployed via .github/workflows/deploy-worker.yml.
+ * Required GitHub secrets:
+ * CLOUDFLARE_API_TOKEN – a token with "Edit Workers" permission
+ * CLOUDFLARE_ACCOUNT_ID – your Cloudflare account ID
+ */
+
+const SECURITY_HEADERS = {
+ // Enforce HTTPS for 1 year, include sub-domains, opt into the HSTS preload list
+ "Strict-Transport-Security": "max-age=31536000; includeSubDomains; preload",
+
+ // Prevent browsers from MIME-sniffing away from the declared content-type
+ "X-Content-Type-Options": "nosniff",
+
+ // Allow framing only from the same origin (defence against clickjacking)
+ "X-Frame-Options": "SAMEORIGIN",
+
+ // Send only the origin (no path/query) for cross-origin requests
+ "Referrer-Policy": "strict-origin-when-cross-origin",
+
+ // Restrict access to powerful browser features that the site does not use
+ "Permissions-Policy":
+ "camera=(), microphone=(), geolocation=(), payment=(), usb=(), interest-cohort=()",
+
+ // Content Security Policy – mirrors the meta tag already in the HTML layout
+ // so that both HTTP-header CSP (enforced by the browser before HTML is parsed)
+ // and the meta tag (fallback) agree on the same policy.
+ "Content-Security-Policy": [
+ "default-src 'self'",
+ "script-src 'self' https://platform.twitter.com",
+ "style-src 'self' https://fonts.bunny.net",
+ "font-src https://fonts.bunny.net",
+ "img-src 'self' data: https://pbs.twimg.com https://abs.twimg.com",
+ "frame-src https://open.spotify.com https://platform.twitter.com",
+ "connect-src 'self' https://syndication.twitter.com",
+ "object-src 'none'",
+ "base-uri 'self'",
+ "form-action 'self'",
+ ].join("; "),
+};
+
+export default {
+ async fetch(request, _env, _ctx) {
+ const response = await fetch(request);
+
+ const newHeaders = new Headers(response.headers);
+ for (const [name, value] of Object.entries(SECURITY_HEADERS)) {
+ newHeaders.set(name, value);
+ }
+
+ return new Response(response.body, {
+ status: response.status,
+ statusText: response.statusText,
+ headers: newHeaders,
+ });
+ },
+};
diff --git a/cloudflare/wrangler.toml b/cloudflare/wrangler.toml
new file mode 100644
index 0000000..524e2ec
--- /dev/null
+++ b/cloudflare/wrangler.toml
@@ -0,0 +1,14 @@
+name = "remotehack-security-headers"
+main = "worker.js"
+compatibility_date = "2024-01-01"
+
+# The worker runs on the remotehack.space zone, intercepting every request
+# so it can attach security headers to every response before it reaches
+# the visitor's browser.
+#
+# account_id is supplied via the CLOUDFLARE_ACCOUNT_ID environment variable
+# in the GitHub Actions deployment workflow – do not hard-code it here.
+
+[[routes]]
+pattern = "remotehack.space/*"
+zone_name = "remotehack.space"