Swiss Public-Sector Security Census

Methodology

This page describes exactly what the census measures, how, and how the results are scored. It is published with every dataset. If something here disagrees with the code, the code is wrong: please open an issue.

Who is measured

GroupCountAuthoritative listDomain source
Municipalities~2,100BFS official commune register, keyed by BFS commune numberWikidata "official website" (P856), then manual review
Cantons26fixed listWikidata, then manual review
Federal offices7 departments, the Federal Chancellery and their officesadmin.ch organisation listmanual

Every entity has a stable id (bfs-<number>, canton-<code>, fed-<abbreviation>), a canton, a web domain (the host of its official website) and a mail domain. The two are tracked separately because many municipalities receive mail on another domain than their site, sometimes a shared cantonal one. Every domain has a recorded source and a verified_on date once a person has checked it.

Schools, hospitals, utilities and other bodies the public sector owns are out of scope for this version.

What is read

For each entity, and nothing else:

  • DNS lookups through a public validating resolver (Quad9 by default);
  • one GET of https://<web domain>/.well-known/security.txt;
  • one GET of the homepage over HTTP and one over HTTPS, following at most 5 redirects;
  • one TLS handshake per protocol version (1.0, 1.1, 1.2, 1.3) on port 443.

No port scanning, no path guessing, no login pages, no forms, no links followed beyond the redirects above. A host or redirect that resolves to a private, loopback or other non-public address is never contacted (the result is an error), and each fetch gives up after 60 seconds in total. At most one request per second reaches any single host. The User-Agent names the project, links to this page and gives a contact address. Response bodies are not stored, except security.txt, which is a public file meant to be read.

Results

Each check gives one of four results:

  • pass: the protection is in place;
  • partial: it is in place but weakened;
  • fail: it is missing, or broken;
  • error: it could not be measured (timeout, resolver failure, missing domain). Errors are retried once after a pause, never count as fails, and are reported separately; an entity with any error is marked *incomplete*.

security.txt (RFC 9116)

PassPartialFail
File present, has Contact and one Expires in the futurepresent but missing Contact or Expires, more than one Expires, an unreadable date, or expiredabsent: 4xx, an HTML page, a page without security.txt fields, or no working HTTPS

Redirects are followed. A signed (OpenPGP cleartext) file is read inside its signature. Expiry is compared with the day of the run.

SPF (RFC 7208), on the mail domain

PassPartialFail
ends in -all or ~all, at most 10 DNS lookups?all or no all; more than 10 lookups; an include with no SPF recordno record, +all, or more than one SPF record

Lookups are counted across include, a, mx, ptr, exists and redirect, following includes and redirects. When there is no all, the redirect target's all is used. Includes that use macros (%{...}) can't be expanded from outside and are not followed.

DMARC (RFC 7489), at _dmarc.<mail domain>

PassPartialFail
p=reject or p=quarantine at pct=100 (the default)p=none (monitoring only), or pct below 100no record, invalid syntax, or more than one record

The record must start with v=DMARC1 followed by p=. Only the exact mail domain is read: a missing record is not looked up on a parent domain.

DNSSEC (RFC 4033), on the zone holding the web domain

PassFail
DS record at the parent, and answers validatenot signed; or signed but broken

The zone is found from the SOA record. "Signed but broken" (the resolver returns SERVFAIL, and answers once validation is switched off; or a DS exists but answers are not authenticated) is tagged dnssec_broken and reported separately: it makes the domain unreachable for anyone behind a validating resolver. There is no partial result.

HSTS (RFC 6797), on the HTTPS homepage

PassPartialFail
HTTP redirects to HTTPS, max-age of at least one year (31536000)header present with a shorter max-ageno header, max-age=0, no valid max-age, HTTP served without redirect, or no working HTTPS

The header is read on the final HTTPS response after redirects. A host that serves nothing on port 80 (refused or silent) counts as redirecting: there is no plain-HTTP page to downgrade to.

TLS (RFC 8996), port 443

PassFail
only TLS 1.2 and/or 1.3 accepted, certificate validTLS 1.0 or 1.1 accepted; the certificate is invalid or expired; neither 1.2 nor 1.3 accepted; port 443 closed

TLS 1.2 and 1.3 handshakes verify the certificate as a browser would. TLS 1.0 and 1.1 handshakes only ask whether the server agrees to the version. If the collector's own TLS library cannot offer 1.0 or 1.1, the result is an error, never a pass.

Recorded, not scored

The MX hosts of every mail domain, to show shared infrastructure: when many municipalities use one provider, a fix there raises many scores at once. Later versions may record MTA-STS (RFC 8461), TLS-RPT (RFC 8460), CAA (RFC 8659) and IPv6 reachability.

DKIM is deliberately left out: its selectors can't be listed from outside, so any measurement would be guesswork.

Score

Each check is worth 2 points for pass and 1 for partial, for a total out of 12. An error scores 0 and marks the entity incomplete. All checks weigh the same; the raw results are published next to the score so anyone can weight them differently (census score <results> --weight dmarc=2).

Publication

Before any named ranking is published:

  1. the census runs and the dataset is frozen;
  2. each canton receives a summary of its own and its municipalities' results, and the Federal Office for Cyber Security (BACS) receives the whole dataset;
  3. 30 days pass, questions are answered and domain mapping errors corrected;
  4. the dashboard and dataset are published with this page;
  5. journalists receive this page and the opt-out and correction process with any numbers.

Corrections and opt-out

Wrong domain? Open a pull request on targets/entities.csv or write to the contact address in the User-Agent. To opt out, write to the same address; the entity id or domain goes into targets/opt-out.csv and is never contacted again.