One login for everything: how a missing dot moved my lab to SSO
Lesson from:
From a password per app and an internal network nobody checked, to one identity, groups as roles, and one door for changing the rules
On 6 May 2026 my single sign-on portal refused to start a session for my own home network. The error was short and completely correct: a cookie domain “must have at least a single period or be an ip address”.
My internal domain had no period in it. It was one word. For more than a month I had been running a login system that, by design, could not log anyone into the part of my lab that only exists at home.
Out of 70 services, only 44 would end up protected on the first day I counted. That one missing dot started the change I want to describe here: from a lab where every service had its own idea of who I was, to one where there is one login, groups decide what you can do, and there is exactly one door through which anyone, human or AI, can change the rules.
Contents · 10 sections
In one minute #
- Single sign-on (SSO) means you log in once, at one portal, and every service trusts that login instead of keeping its own passwords.
- In April 2026 my portal protected the public services. Inside my home network, many services had no login at all, or their own separate accounts.
- On 6 May I moved the whole internal network to a domain that cookies can work on, and on the first startup the manager automatically protected 37 internal routes. Coverage that day: 44 of 70 services.
- Over the next months the rules moved into one place: a manager that owns users, groups and policies, generates the configuration and commits it to Git.
- In October the last step: AI agents may change access only through that manager, with their own token, and only in one direction, tighter.
The before: one portal, many doors #
By the start of April I already had Authelia, an open-source SSO and two-factor portal, in front of my public services. The routing layer, Traefik, sends each request to the portal first; the portal answers “yes”, “log in first” or “no”; only then does the request reach the app. This pattern is called forward authentication.
The runbook from 2 April shows the shape of things, in two steps that look harmless:
- create the public route with the authentication middleware;
- “create matching internal route without authelia middleware”.
Every service had two addresses. The public one asked who you were. The internal one did not, because the internal network was home, and home was trusted.
The other half of the picture was the apps that kept their own accounts. A classroom app with its own user table and its own passwords on top of the portal. A mail server with its own logins, as mail servers must have. Database admin pages with their own admin accounts. The classroom’s first version, the record says later, “had two auth layers (Authelia + app DB passwords)”.
If you run a few self-hosted apps, you know this landscape. Every new app is a new login page. Every login page is a password you either reuse or forget.
The trigger: a cookie needs a dot #
A browser session cookie is tied to a domain. Set it on example.com and it works on every subdomain. But browsers, and Authelia, reject a cookie domain with no dot at all, because a single word looks like a top-level domain, and no website should be able to set a cookie for all of .com.
My internal names were of the form service.<word>. One word after the dot. The portal could protect the public names, but it could not keep a session for the internal ones, so it simply was not used there.
The fix was a migration, done on 6 May in one go, and the record lists it like a checklist:
- a new wildcard certificate for a two-level internal domain, signed by my local certificate authority;
- new DNS entries on the local resolver;
- a session cookie in the portal for the new domain;
- 60+ parallel routes, so every service answered on both the old and the new name at once;
- about 95 code references updated across 36 files.
Outcome: “All 64+ services accessible on [the new domain]. Authelia SSO functional. Zero downtime migration.”
A migration where old and new run side by side has a particular quality: nothing breaks, and nothing is finished. The old names stayed alive as a safety net; the record keeps their removal as a separate, later phase.
The build: making “protected” mean something #
Day one: 62.9 per cent #
The same day, I rolled out a set of improvements to the manager, the small web app I use to manage the portal’s users, groups and rules. One of them was auto-protect: on startup it puts the authentication middleware on every internal route that is registered as protected.
On its first run it protected 37 internal routes. The dashboard showed the honest number: 62.9% coverage, 44 of 70 services protected.
It is the most honest number in this story: what a home lab looks like on the day you start counting.
Rules as data, not as a file #
The portal reads its access rules from a configuration file. It is tempting to edit that file by hand. It happened once, and it is the most useful mistake in this story.
On 18 September a rule for a read-only reader group was added directly in the generated file, in the repository and on the live disk, and the portal was restarted. The first edit even broke the YAML indentation. The rule worked. And it would have silently disappeared at the next apply, because the file is not the source of truth. The manager’s database is: services, groups and the policy that connects them. The file is its output.
The lesson in the record is the rule I follow now: “Any Authelia change (ACL, users, groups, service policy) MUST go through [the manager].” The manager owns the backup, the validation, the reload, the Git commit and push, the audit log and the notification. The file even says so in its header: do not edit manually.
When “protected” was not protected #
In July a consistency scan found three services that the manager listed as protected, with two-factor required, while their live routes had no authentication middleware at all. They answered anyone with HTTP 200. The policy was right. It just never ran.
That class of failure, and five other traps around forward authentication, cookie domains and OIDC refresh tokens, is the subject of Authelia SSO pitfalls: a policy protects nothing if the route skips it. What matters for this story is what changed afterwards: the manager got a guardrail that compares what it believes with what the cluster actually routes, per domain, and sends an alert when they disagree. The next scan: 75 of 75 in sync.
It also learned a distinction I had got wrong: services that have their own login, like mail clients on a phone or meeting join links, must not be put behind the portal. Gating them breaks how they work. One login for everything does not mean one login in front of everything.
The technical heart: groups are the roles #
The idea that made SSO more than a convenience is groups.
When a request passes the portal, Authelia forwards the identity to the app as headers: who the user is, and which groups they belong to. An app that trusts those headers, and only from the proxy, no longer needs its own user table. It needs a mapping from groups to roles.
My classroom was rebuilt this way on 14 October. Its whole access model now fits in a few lines. Here is the generic shape:
# Portal side: who may reach the service at all, and how strongly they must prove it
access_control:
rules:
- domain: "classroom.example.com"
subject: ["group:teachers", "group:students", "group:classroom-admins"]
policy: one_factor
- domain: "admin.example.com"
subject: "group:admins"
policy: two_factor
# App side: roles come from the groups header, never from a local password table
def role_from_headers(headers):
groups = set(headers.get("Remote-Groups", "").split(","))
if groups & {"admins", "classroom-admins"}:
return "admin"
if "teachers" in groups:
return "teacher"
if "students" in groups:
return "student"
return None
Two details make this safe, and both are easy to miss. First, the app must only be reachable through the proxy, otherwise anyone can send a fake Remote-Groups header; a network policy allows only the routing layer to reach the classroom. Second, on the few public paths that do not go through the portal, such as registration and invite links, the proxy strips those headers and the app ignores them.
The classroom itself, with meeting rooms that open only with a token issued for that room, is described in A self-hosted classroom with SSO-only login and token-locked Jitsi.
The effect for a person is simple. A new student gets one account, in one place, in one group. They can log in to the classroom, see the library materials their group allows, and nothing else. When they leave, one account is disabled and every door closes at once.
The last door: AI agents #
By October, AI agents were doing a lot of work in my lab, including work on access rules. That raised an uncomfortable question. If an agent can edit the portal’s configuration, it can also open things.
On 17 October I made it a rule in code. My instruction in the record is one line: “agents change Authelia only through the Manager. One token per caller.”
The manager got a small service API for agents with three properties:
- one token per caller, so the audit log shows which agent did what;
- tighten-only: an agent can move an open service behind the portal and raise a group’s grant, but it can never open a service, add a bypass or lower a policy;
- a never-protect list for public pages and machine clients, so an over-eager agent cannot lock out the things that must stay reachable.
At launch it found 19 open services it was allowed to protect. The same day, nine data-bearing and admin services on the internal network were moved behind two-factor login for administrators only. Before changing them, four days of logs were checked: only browsers used those addresses, so no machine client would break. And the internet-facing and home-facing entrances of the routing layer were split, so that the manager can show what the internet can actually reach. That split is in DNS is not a firewall: splitting Traefik into LAN and public.
What it cost #
SSO concentrates risk. If the portal is down, everything is down. If the portal is wrong, everything is wrong at once.
I learned the first part through my Kubernetes dashboard, which uses the portal as its identity provider and has a token-refresh loop with no backoff. Twice in August it locked me out of my own login, once 6 hours and 23 minutes after the previous fix. A watchdog now restarts it when it starts flooding. The story is in How my Kubernetes dashboard locked me out of my own SSO.
The second part is why the guardrail and the single door matter more than any individual rule. When one system decides who gets in everywhere, it has to be checked against reality continuously, and changed in only one way.
It also cost something softer: the internal network stopped being a place where everything just opens. Services that once simply appeared now ask who you are. They all ask the same portal.
What changed #
- Before: public services behind a portal, internal copies with no login, and apps with their own passwords.
- The trigger: a cookie domain without a dot, which made SSO impossible on the internal network.
- After: one identity for every service, two-factor for admin and data-bearing services, roles derived from groups.
- One source of truth: the manager’s database generates the rules, commits them and alerts on drift.
- Agents: one token each, tighten-only, through the manager or not at all.
Questions people ask me #
What is single sign-on, and do I need it at home? #
Single sign-on means one login at one portal, trusted by every service behind it. At home it pays off as soon as you have more than a handful of self-hosted apps, or anyone besides you uses them. Instead of a password per app, you have one account per person, with two-factor where it matters, and one place to disable it.
Why did my SSO portal reject my internal domain? #
Browsers and most portals refuse cookie domains without a dot, such as a bare one-word name, because they look like top-level domains. Use a two-level internal domain, such as home.example, with its own certificate, and the session cookie will work across all internal services.
Should every service be behind the SSO portal? #
No. Services with their own client protocols, such as mail apps on a phone, or links meant for guests, break when a browser login page is put in front of them. Protect them with their own authentication and keep the portal for web interfaces. One login for everything people use in a browser, not one gate in front of every port.
How do you let AI agents manage access safely? #
Give them a narrow API instead of the configuration files: one token per agent, every change audited, and only changes that make access stricter. Opening a service or lowering a policy stays a human decision, made in the admin interface.
The closing #
The turning point was a single dot. Without it, the portal could not hold a session at home, and “home is trusted” was not a decision I had made, it was a limitation I had not noticed.
The change since then has not been about more locks. It has been about fewer sources of truth: one identity per person, one place where groups meet services, one door through which rules change. If your own setup has a password per app and a file you edit by hand when something needs to open, the first step is not a new tool. It is counting, honestly, how many of your services are actually protected. Mine was 44 of 70.