The Key That Reached Too Much: Least Privilege for Self-Hosters
On 5 October 2026, Denmark's CPR Administration disclosed that unauthorised parties had gained access to the names, addresses and CPR numbers of about 8.8 million registered persons in the CPR, the country's central national register. The CPR number is the personal identification number used across Danish public and private services. That is why access to the register is regulated, and why an exposure on this scale is serious.
The detail that matters most is how it happened. According to the Danish Ministry of Research, Education and Digitalisation, the access came from misusing a private Danish company's lawful access to search the register. Nothing was broken into. An approved key was used for the wrong purpose, and that is exactly why the case is worth studying if you run any system of your own.
What least privilege access means when one account can query a whole register
Least privilege is a simple rule: every account, API key and service gets the smallest set of permissions it needs to do its job, and nothing more. A backup job can write to one bucket. A dashboard can read a few tables. Neither can do anything else.
In Denmark, private companies may query the CPR within limits set by section 38 of the CPR Act. The ministry says this misuse happened inside the scope of what private companies are permitted to query. I do not know the details of that company's access, and the general lesson does not depend on them.
A login proves who is asking. It says nothing about whether the question is reasonable. Strong passwords and two-factor authentication protect the door, but once a credential is through it, its scope decides how much damage a wrong query can do. That reach is the blast radius.
Blast radius matters more than login strength because an approved credential can be stolen, misused by someone who legitimately holds it, or fed to a buggy script that loops over every row. In each case, the damage stops at whatever the credential can reach. Your login decides who gets in. Your permissions decide what a misuse costs.
What we actually know, and what "8.8 million" means
Numbers like this get garbled quickly, so here is what the CPR Administration and the ministry stated on 5 October 2026:
- On the evening of Friday 2 October 2026, the CPR Administration became aware of irregular activity in the CPR system during September.
- Over the weekend it established that unauthorised parties had gained access to the names, addresses and CPR numbers of about 8.8 million registered persons.
- The access was obtained by misusing a private Danish company's lawful access to search the register. The company has not been named.
- The CPR Administration has stopped the company's access and reported the incident to the Danish Data Protection Agency (Datatilsynet). The police are investigating together with the relevant authorities.
- The responsible minister, Christina Egelund, called it a "deeply serious incident" and said a thorough security review of the CPR system has been ordered.
The 8.8 million figure is a count of records, not of living Danes. The ministry says it covers living, emigrated and deceased persons, and the register holds about 11 million registered persons in total. A headline that turns this into "8.8 million citizens" gets it wrong.
The unauthorised access also does not include the names and addresses of people registered with name and address protection. For anyone designing access rules, that is a reminder that some data deserves a narrower scope than the rest.
Finally, the investigation is at an early stage. The ministry says it is not currently possible to say who is behind it. The official statements describe unauthorised access, and that is the claim I will stick to here.
Applying least privilege to your own stack
You do not run a national register, but you almost certainly have credentials that reach more than they need to. In my own setup, the ones I trust least are the ones I created in a hurry. This is the checklist I use.
1. Scope every key to the narrowest thing it needs
Before you create a credential, write down what it is for in one sentence. Then grant only that. If Grafana needs to chart one table, give it a database role that can read that table and nothing else:
CREATE ROLE grafana_ro LOGIN PASSWORD 'change-me';
GRANT CONNECT ON DATABASE metrics TO grafana_ro;
GRANT USAGE ON SCHEMA public TO grafana_ro;
GRANT SELECT ON public.readings TO grafana_ro;The same idea applies to object storage. A backup tool needs write access to one bucket in MinIO or S3, not an admin key for the whole server.
2. Prefer read-only
Most integrations only read: monitoring, dashboards, search indexers and many AI tools. If a tool never writes, a read-only credential removes a whole class of damage.
Watch for hidden write access too. Mounting /var/run/docker.sock into a container gives that container control of the Docker daemon, which is effectively root on the host. If a tool only needs to list containers, put a socket proxy in front of it that allows just those API calls.
AI agents with tool access need this even more, because they decide at runtime which calls to make. I covered their controls in my earlier post on securing self-hosted AI agents.
3. Give each integration its own credential
Sharing one database user between Nextcloud, n8n and a cron script is convenient until something goes wrong. Then you cannot tell which one made a strange query, and you cannot cut one off without breaking the others.
4. Expire and rotate
Many token systems let you set an expiry date at creation. Use it. For credentials that cannot expire on their own, such as database passwords, put a rotation date in your calendar.
5. Review the approvals you made years ago
This is the step almost everyone skips. Keep a simple inventory, even a text file, with one line per credential: what it is called, what it can reach, what uses it, when it was created and when it expires. Review it every few months. If you cannot explain why a key exists, disable it and see what complains.
The half people skip: knowing what normal looks like
The CPR Administration noticed irregular activity and stopped the access. That sequence is the second half of least privilege. Scoping limits what a misused key can reach, but a well-scoped key can still be abused within its scope. Visibility tells you when that is happening, so you can act. That means knowing what normal looks like for each key.
- Log queries per credential. PostgreSQL can log connections and statements, and the pgAudit extension adds more detail. Reverse proxies such as Caddy, Traefik and Nginx can log every request, and MinIO has an audit log. Make sure each log line shows which credential made the request.
- Watch for volume that is out of pattern. If a dashboard key that normally reads a handful of rows an hour suddenly reads a whole table, you want to know. A simple alert on requests per key per hour, in Prometheus and Grafana or a small script over your logs, catches a lot.
- Be able to revoke a single key without taking the system down. This is where one credential per integration pays off. Practise it once: revoke a key on purpose, see what breaks, and write down the steps. When you need to do it for real, it should take minutes.
If you want a second pair of eyes
Least privilege is a habit you keep, week after week. Scope each key, prefer read-only, separate credentials, expire what you can, and read your logs. A misused key then becomes a smaller and shorter problem.
If you would rather have someone review the access your self-hosted stack hands out, see the services page. You can also find me on Fiverr: hiteshsaini459 · Upwork: hiteshsaini25.