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:

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

CheckBeforeRisk
HSTSmissingA first visit over plain HTTP can be intercepted and downgraded.
Content-Security-PolicymissingNo limit on where scripts, styles and fonts can load from if anything is ever injected into the page.
Clickjacking protectionmissingAny site could load ours in an invisible frame.
Minimum TLS version1.0TLS 1.0 and 1.1 are deprecated (RFC 8996) and flagged by every scanner.
Cloudflare SSL modeFullCloudflare encrypted to the origin but didn't validate its certificate.
security.txtmissingNo published way to report a vulnerability.
DMARCp=noneForged mail from @cyconic.com was reported, not stopped.
Third-party fontsGoogle FontsAn 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.

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:

ScannerResult
Mozilla ObservatoryA+, score 130, 12 of 12 tests passed
securityheaders.comA+

You can rerun Mozilla's scan yourself: developer.mozilla.org/en-US/observatory/analyze?host=cyconic.com.

Takeaways

questions or corrections: [email protected]