Date: 2026-08-10 Status: Reasonably secure for what this actually is. No critical issues found.
This is a personal portfolio site with a static blog. Security is straightforward because there's no user authentication, database, or server-side logic on the live site.
Actual infrastructure (verified 2026-08-10):
- Hosting: GitHub Pages — static files only, no server, no Azure
- Deploy: GitHub Actions (
.github/workflows/publish-gh-pages.yml) builds the Blazor WASM app and publishes to Pages on every push tomain - DNS: domain registered elsewhere, DNS records managed via a separate DNS panel, pointed at GitHub Pages via a
CNAMEfile inwwwroot/ - BackendTools: a separate Blazor Server app, run locally only (
dotnet run), never deployed anywhere
An earlier version of this document assumed Azure App Service hosting (Azure Portal custom headers, Web.config, Azure Key Vault). None of that applies. If you read older recommendations referencing Azure elsewhere in this repo's history, they don't reflect reality — GitHub Pages has no server to configure.
- ✅ HTTPS/TLS — GitHub Pages enforces HTTPS on the custom domain automatically
- ✅ No secrets in code — verified via repo-wide scan for API key patterns, private key blocks; none found
- ✅ No
.envor per-environmentappsettings.*.jsontracked — and now actively gitignored (see below) - ✅ XSS prevention — no inline event handlers; the one
MarkupStringusage (blog post HTML content) renders content generated by the Claude API from your own input, not arbitrary user input - ✅ Blazor WebAssembly — client-only, nothing to exploit server-side because there is no server
- ✅ File upload safety (BackendTools, local-only) — images capped at 5MB, format-validated
- ✅ No
.user/build-artifact files accidentally tracked in git
- 🟡 "Contact form" is a
mailto:link, not a real form.Contact.razorandConsultingPage.razorboth link tomailto:doug.rosenberg@gmail.com— no submission is ever sent to or stored by this app. That means: no server-side validation/rate-limiting/CAPTCHA is needed because there's no submission endpoint to abuse, but it also means you never capture a lead's info — visitors have to actually send the email themselves. If you want submissions captured, that's a feature decision (e.g. a form service like Formspree, or a small Azure Function), not a security fix. - 🔴 Security headers (CSP, X-Frame-Options, HSTS, etc.) are NOT achievable via GitHub Pages configuration. GitHub Pages serves static files with a fixed set of response headers you cannot customize — no Web.config equivalent, no
_headersfile support (that's a Netlify/Cloudflare Pages feature), no portal setting. The only way to add these headers todougrosenbergdev.comis to put a reverse proxy in front of GitHub Pages — most commonly by moving DNS management to Cloudflare (free tier) and using a Transform Rule or Worker to inject headers on the way through. This is a real infrastructure change, not a config toggle. See "If You Want Security Headers" below if you decide it's worth it.
| Risk | Level | Mitigation | Status |
|---|---|---|---|
| XSS via blog content | Low | Content originates from your own writing via the Claude API, not third-party input; rendered via MarkupString |
✅ Acceptable |
| JSON file corruption (blog-posts.json) | Low | Versioned in git; restorable from history | ✅ Mitigated |
| Repo access | Low | GitHub auth + local pre-push hook blocking direct pushes to main |
✅ Mitigated |
| BackendTools API key exposure | Low | Never deployed; runs locally; key supplied via env var/user-secrets, not committed | ✅ Mitigated |
Accidental secret commit (.env, appsettings.Development.json) |
Low | Explicitly gitignored as of 2026-08-10 (previously not excluded — no leak found, but the gap existed) | ✅ Fixed |
| Missing HTTP security headers | Medium | Not achievable without a reverse proxy in front of GitHub Pages | 🟡 Deferred — see below |
| No lead capture from "contact form" | N/A (product, not security) | mailto: link only |
⚪ By design, unless changed |
This requires putting something in front of GitHub Pages that can rewrite the response. The lowest-effort path:
- Move DNS management for
dougrosenbergdev.comto Cloudflare (free tier) — this means changing nameservers at your registrar to Cloudflare's, then re-adding your existing DNS records (A/CNAME to GitHub Pages, the Google Search Console TXT record, any email records) inside Cloudflare's dashboard. - Enable Cloudflare proxying (orange cloud) on the record pointing at GitHub Pages.
- Add a Cloudflare Transform Rule (Rules → Transform Rules → Modify Response Header) to inject:
(The CSP above is a starting point based on what this site actually loads — Bootstrap CDN, Google Fonts, GitHub stats iframes — not a copy-paste-and-forget value. Test it in report-only mode first.)
X-Content-Type-Options: nosniff X-Frame-Options: DENY Referrer-Policy: no-referrer-when-downgrade Strict-Transport-Security: max-age=31536000; includeSubDomains Content-Security-Policy: default-src 'self'; script-src 'self' https://cdn.jsdelivr.net https://fonts.googleapis.com; style-src 'self' 'unsafe-inline' https://fonts.googleapis.com; font-src 'self' https://fonts.gstatic.com; img-src 'self' data: https:; connect-src 'self' https://github-readme-stats.vercel.app
Is it worth it? For a static portfolio with no auth, no database, no user-submitted forms, and no third-party scripts beyond fonts/CDN CSS, the actual risk these headers mitigate is low. It's a reasonable "someday" item, not an urgent one. Re-evaluate if the site ever adds a real form, user accounts, or third-party embeds beyond what's here today.
ANTHROPIC_API_KEY(orAI:AnthropicApiKeyin config terms) — set via environment variable ordotnet user-secrets, never committed.BackendTools/appsettings.jsonintentionally contains no secrets (just logging config)..gitignorenow excludes.env,.env.*, andappsettings.*.json(withappsettings.jsonitself explicitly un-ignored, since it's checked in and contains no secrets) — this protects against ever accidentally committing a real key to a per-environment settings file.
Do not do this without:
- Adding authentication (Bearer token or OAuth)
- Rate limiting per IP
- Moving the API key to a real secrets manager appropriate to wherever you deploy it (this project has no Azure account, so "Azure Key Vault" isn't a given — pick whatever your actual host provides)
- HTTPS only
- Request logging for an audit trail
- ✅ SQL Injection — no database used
- ✅ Unauthorized publishes — blog posts are static JSON, not executable, and only reach production via a PR merge to
main - ✅ User authentication — no users, no auth needed
- ✅ CSRF — no state-changing server endpoints exist to forge a request against
dotnet outdated
dotnet package update --interactiveKeep .NET, Bootstrap, MudBlazor, and all packages current. No fixed cadence has been established — worth doing whenever touching either project meaningfully.
This is a personal portfolio project. Security issues can be reported directly to doug.rosenberg@gmail.com.
Last updated: 2026-08-10 Next review: whenever the site's actual capabilities change (e.g. a real contact form, user accounts, third-party embeds)