CYC-001 · 2026-10 · web / email
Auditing our own website
Before we publish anything about how other systems should be secured, our own site should pass the checks we'd run on someone else's. It didn't. This is what we found on cyconic.com, what we changed, and how we verified it, ending with A+ on both Mozilla Observatory and securityheaders.com.
The setup
cyconic.com is a static site: plain HTML, CSS and a little JavaScript, no framework and no build step. It's deployed by Cloudflare Pages from a GitHub repository, sits behind Cloudflare's proxy, and uses Zoho Mail for email. Everything below works on Cloudflare's free plan.
What we checked
Four things a visitor, a scanner or an attacker can see from outside without any access:
- HTTP security headers. What the browser is told to enforce.
- TLS. Which protocol versions the server still accepts, and whether HTTPS is enforced.
- Disclosure. Whether a researcher who finds a bug knows where to report it.
- Email authentication. Whether someone can send mail that claims to be from @cyconic.com.
Headers first. One request shows everything the server sends back:
curl -sI https://cyconic.com | grep -iE 'strict-transport|content-security|x-frame|x-content-type|referrer|permissions'
Only two came back:
x-content-type-options: nosniff
referrer-policy: strict-origin-when-cross-origin
For email, the policies are public DNS records, so anyone can read them:
dig +short TXT cyconic.com # SPF
dig +short TXT zmail._domainkey.cyconic.com # DKIM (Zoho's selector)
dig +short TXT _dmarc.cyconic.com # DMARC
What we found
| Check | Before | Risk |
|---|---|---|
| HSTS | missing | A first visit over plain HTTP can be intercepted and downgraded. |
| Content-Security-Policy | missing | No limit on where scripts, styles and fonts can load from if anything is ever injected into the page. |
| Clickjacking protection | missing | Any site could load ours in an invisible frame. |
| Minimum TLS version | 1.0 | TLS 1.0 and 1.1 are deprecated (RFC 8996) and flagged by every scanner. |
| Cloudflare SSL mode | Full | Cloudflare encrypted to the origin but didn't validate its certificate. |
| security.txt | missing | No published way to report a vulnerability. |
| DMARC | p=none | Forged mail from @cyconic.com was reported, not stopped. |
| Third-party fonts | Google Fonts | An external dependency on every page load, and a looser CSP to allow it. |
Some things were already right: plain HTTP redirected to HTTPS, www redirected to the bare domain, and SPF and DKIM were in place for Zoho.
What we changed
1. HTTPS and TLS, in Cloudflare
These are zone settings, so we set them through Cloudflare's API with a short-lived token scoped to this one domain, then deleted the token.
- HSTS: on,
max-agesix months,includeSubDomains. We checked first that the only subdomain,www, already serves HTTPS. We left preload off; it's hard to undo, and we can add it once we're sure. - Always Use HTTPS: on, so the redirect happens at Cloudflare's edge.
- Minimum TLS: 1.2.
- SSL mode: Full (strict). Our origin is
*.pages.dev, which has a valid certificate, so strict validation costs nothing.
2. Security headers, in a _headers file
Cloudflare Pages reads a _headers file from the repository and attaches those headers to responses. That keeps them in version control next to the code they protect:
/*
Content-Security-Policy: default-src 'self'; script-src 'self' https://static.cloudflareinsights.com; style-src 'self'; img-src 'self' data:; font-src 'self'; connect-src 'self' https://cloudflareinsights.com; base-uri 'self'; form-action 'none'; object-src 'none'; frame-ancestors 'none'; upgrade-insecure-requests
X-Content-Type-Options: nosniff
X-Frame-Options: DENY
Referrer-Policy: strict-origin-when-cross-origin
Permissions-Policy: camera=(), microphone=(), geolocation=(), payment=(), usb=(), interest-cohort=()
Cross-Origin-Opener-Policy: same-origin
The CSP has no 'unsafe-inline' anywhere. To make that possible we moved every inline <style> and <script> into style.css and main.js, and self-hosted our fonts instead of loading them from Google. The one outside origin we allow is Cloudflare Web Analytics, which Cloudflare injects into the page.
frame-ancestors 'none' is the modern clickjacking defence, and it only works as a real HTTP header, not in a <meta> tag. X-Frame-Options: DENY covers older browsers.
The same file also tells search engines not to index the *.pages.dev copies of the site, so only cyconic.com shows up in results:
https://:project.pages.dev/*
X-Robots-Tag: noindex
3. security.txt
/.well-known/security.txt (RFC 9116) tells researchers where to send a vulnerability report. Ours points to a dedicated security@ address and has to be renewed before it expires:
Contact: mailto:[email protected]
Expires: 2027-10-21T00:00:00.000Z
Preferred-Languages: en
Canonical: https://cyconic.com/.well-known/security.txt
4. DMARC
Moving DMARC from p=none to p=quarantine tells receiving servers to send forged mail from our domain to spam. Doing it before checking that your own mail passes is how you send your own mail to spam, so we checked first. We sent a message from our Zoho mailbox to Gmail and read the headers with Show original:
spf=pass smtp.mailfrom=…@cyconic.com
dkim=pass [email protected] header.s=zmail
dmarc=pass header.from=cyconic.com
All three passed and aligned with our domain, so we changed the record:
v=DMARC1; p=quarantine; rua=mailto:…@dmarc-reports.cloudflare.net
The rua address sends aggregate reports to Cloudflare's DMARC dashboard. Once a few weeks of reports show nothing legitimate failing, the next step is p=reject.
Verifying it
The same header request now returns the full set:
strict-transport-security: max-age=15552000; includeSubDomains
content-security-policy: default-src 'self'; … frame-ancestors 'none'; upgrade-insecure-requests
cross-origin-opener-policy: same-origin
permissions-policy: camera=(), microphone=(), geolocation=(), …
referrer-policy: strict-origin-when-cross-origin
x-content-type-options: nosniff
x-frame-options: DENY
TLS 1.1 is refused:
curl --tlsv1.1 --tls-max 1.1 https://cyconic.com # handshake fails
And two independent scanners agree:
| Scanner | Result |
|---|---|
| Mozilla Observatory | A+, score 130, 12 of 12 tests passed |
| securityheaders.com | A+ |
You can rerun Mozilla's scan yourself: developer.mozilla.org/en-US/observatory/analyze?host=cyconic.com.
Takeaways
- A static site isn't automatically a secure one. No database and no login doesn't mean no attack surface: framing, downgrade attacks and spoofed email all apply.
- A strict CSP is mostly cleanup. The policy itself is one line. The work is moving inline code into files and cutting third-party dependencies, which also made the site faster.
- Check before you tighten. HSTS with
includeSubDomainsand DMARC enforcement can both break things you forgot about. Look at your subdomains and your mail headers first. - Keep config in the repo where you can. The
_headersfile is reviewable and versioned in a way that dashboard settings aren't. - The whole thing took an afternoon and cost nothing on Cloudflare's and Zoho's free plans.
questions or corrections: [email protected]