Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions docs.json
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,17 @@
"repositories"
]
},
{
"group": "Explainers",
"pages": [
"explainers/overview",
"explainers/what-is-a-hypervisor",
"explainers/clusters-and-quorum",
"explainers/staying-up-when-a-machine-dies",
"explainers/one-login-for-everything",
"explainers/secrets-that-expire"
]
},
{
"group": "Architecture",
"pages": [
Expand Down
91 changes: 91 additions & 0 deletions explainers/clusters-and-quorum.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
---
title: "Clusters and quorum"
description: "Why several hypervisor hosts are joined into one system, and why the number of votes matters more than the number of machines."
page_type: explanation
---

> Two machines that disagree about who is in charge will each confidently do the wrong thing. Quorum is how you stop that.

A **cluster** is several hypervisor hosts managed as one system. You get one place to see everything, guests can move between hosts, and — the real prize — the cluster can react automatically when a host disappears.

Joining machines together creates a problem that does not exist with one machine: they now have to agree on things, and the network between them can fail.

## The split-brain problem

Picture two hosts and a network cable between them. The cable is cut. Neither host is broken; each simply cannot reach the other.

From inside each host, that is indistinguishable from *the other one died*. So each concludes it is the survivor and starts the guests it thinks were lost. Now the same guest is running twice, on two hosts, both writing to what they believe is the same disk.

That is **split-brain**, and it corrupts data rather than merely causing downtime. It is the failure that cluster design exists to prevent.

{/* Shape: parallel convergence. The severed link forces two independent verdicts. 5 nodes. */}
{/* Subgraphs are physical machines — a real co-location, not a role grouping. */}

```mermaid
%%{init: {'theme':'base','look':'handDrawn','themeVariables':{'fontFamily':'Geist','fontSize':'14px','primaryColor':'#102937','primaryTextColor':'#F4EFE6','primaryBorderColor':'#4FB3A9','lineColor':'#4FB3A9','secondaryColor':'#0B1D2A','tertiaryColor':'#1A2A38','clusterBkg':'rgba(79,179,169,0.08)','clusterBorder':'#4FB3A9'}}}%%
flowchart LR
subgraph A["node-a"]
HostA([Hypervisor A])
GuestA([Guest, running])
end

Link{Network link}

subgraph B["node-b"]
HostB([Hypervisor B])
GuestB([Guest, also running])
end

HostA --> Link
Link --> HostB
HostA --> GuestA
HostB --> GuestB

classDef host fill:#102937,stroke:#4FB3A9,stroke-width:2px,color:#F4EFE6;
classDef gate fill:#102937,stroke:#E06B4A,stroke-width:2.5px,color:#F4EFE6;
classDef sink fill:#102937,stroke:#F4EFE6,stroke-width:2.5px,color:#F4EFE6;

class HostA,HostB host
class Link gate
class GuestA,GuestB sink

linkStyle 0,1 stroke:#E06B4A,stroke-width:2px,stroke-dasharray:4 4;
```

## Quorum is the fix

Each host gets a vote. A host may only run guests if it can see **more than half** of all votes. Not half — *more than* half.

With four hosts, four votes, a majority is three. Split them two-and-two and neither side has three, so **both sides stop**. That sounds like a worse outcome than split-brain until you remember the alternative was silent data corruption. Downtime is recoverable; two writers on one disk often isn't.

| Hosts | Votes needed | Can survive losing |
| --- | --- | --- |
| 2 | 2 | nothing — losing either stops the cluster |
| 3 | 2 | one host |
| 4 | 3 | one host |
| 5 | 3 | two hosts |

<Warning>
Read the two-host row carefully. Adding a second machine for redundancy, with no other change, makes things **worse** — you go from one machine that can fail to two machines that each can take the cluster down. This surprises nearly everyone.
</Warning>

## Why odd numbers

Four hosts survive exactly as many failures as three do, and cost you an extra machine to reach that identical result. Five survive two. So the useful counts are odd, and the sizes that actually buy something are 3 and 5.

An even count is not wrong — it is simply paying for a machine that adds no fault tolerance. Where an even count exists, it is usually because the extra host was added for capacity rather than resilience, which is a perfectly good reason.

<Note>
Some clusters solve the two-host case with a **witness** — a tiny third voter that holds no guests and exists only to break ties. It can be something as small as a Raspberry Pi. Three votes, two machines' worth of hardware.
</Note>

## Where to go next

<CardGroup cols={2}>
<Card title="Staying up when a machine dies" icon="heart-pulse" href="/explainers/staying-up-when-a-machine-dies">
Quorum decides who may act. This is what they actually do.
</Card>
<Card title="What a hypervisor is" icon="layer-group" href="/explainers/what-is-a-hypervisor">
The layer underneath, if clusters are getting ahead of you.
</Card>
</CardGroup>
89 changes: 89 additions & 0 deletions explainers/one-login-for-everything.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
---
title: "One login for everything"
description: "What single sign-on actually does when you click a service, and why one login for twenty apps is safer than twenty passwords."
page_type: explanation
---

> Twenty self-hosted apps means twenty login screens, twenty password policies, and twenty chances for one of them to store your password badly.

Single sign-on replaces all of that with one identity that every application trusts. You log in once. Every service afterwards recognises you without ever seeing a password.

The instinctive objection — *isn't one login a single point of failure?* — is worth taking seriously, and the answer is the opposite of what it looks like.

## Why one login is safer than twenty

Twenty applications each storing your password means twenty implementations of password hashing, twenty databases that could leak, and twenty places a reused password becomes a breach everywhere else. Self-hosted software varies enormously in how carefully it does this, and you generally cannot tell which is which from outside.

With SSO, only one system ever handles the password. That one system can afford strong protections — hardware-backed second factors, passkeys, rate limiting, real audit logging — because you configure it once rather than twenty times. The applications never see the password at all, so they cannot leak it.

## What happens when you click

There are two mechanisms doing different jobs, and both are usually present.

**A gate at the front door.** A reverse proxy sits in front of every service. Before passing a request through, it asks the identity provider: is this person logged in? If not, the request never reaches the application — it is redirected to a login page instead. The application isn't merely unauthenticated at that point; it is unreachable.

**An identity handshake.** Once past the gate, the application often wants to know *who* you are, to show your own dashboard. It bounces you to the identity provider, which hands back a signed statement — you are this person, in these groups. Because it is signed, the application can verify it without trusting the browser.

{/* Shape: linear chain with a gate. 6 nodes covering both mechanisms in one pass. */}
{/* Boundary crossings: 0. The identity provider is reached from the gate, not the app. */}

```mermaid
%%{init: {'theme':'base','look':'handDrawn','themeVariables':{'fontFamily':'Geist','fontSize':'14px','primaryColor':'#102937','primaryTextColor':'#F4EFE6','primaryBorderColor':'#4FB3A9','lineColor':'#4FB3A9','secondaryColor':'#0B1D2A','tertiaryColor':'#1A2A38','clusterBkg':'rgba(79,179,169,0.08)','clusterBorder':'#4FB3A9'}}}%%
flowchart LR
You((You))
Proxy([Reverse proxy])
Gate{Logged in?}
Idp([Identity provider])
App([Application])
Done([Your dashboard])

You --> Proxy
Proxy --> Gate
Gate -->|no| Idp
Idp --> Gate
Gate -->|yes| App
App --> Done

classDef external fill:#102937,stroke:#E6B35A,stroke-width:2px,color:#F4EFE6;
classDef host fill:#102937,stroke:#4FB3A9,stroke-width:2px,color:#F4EFE6;
classDef gate fill:#102937,stroke:#E06B4A,stroke-width:2.5px,color:#F4EFE6;
classDef sink fill:#102937,stroke:#F4EFE6,stroke-width:2.5px,color:#F4EFE6;

class You external
class Proxy,Idp,App host
class Gate gate
class Done sink
```

## The header trap

The gate tells the application who you are by adding a header to the request — something like `Remote-User: you`. The application trusts it completely.

Which means: if a browser could *send* that header itself, anyone could claim to be anyone.

So the proxy must **strip those headers from every incoming request before doing anything else**, then re-add the real values only after authenticating. Order matters absolutely here. Strip-then-authenticate is secure. Authenticate-then-strip erases the real identity and leaves the forged one. The two configurations look nearly identical and behave completely differently.

<Warning>
This is worth testing rather than assuming. Send a request with a forged identity header to a gated path and confirm you are challenged rather than logged in. It is a one-line check that catches a total authentication bypass.
</Warning>

## Machines don't use this

API clients, scripts and integrations do not log in through a browser, so making them traverse a login page breaks them. They authenticate with their own credential — a token scoped to what that client may do — and bypass the browser gate entirely.

That bypass is exactly why the header strip has to be unconditional. If an API path skips the gate, and headers are only stripped on gated paths, then the API path will happily accept a forged identity.

<Note>
Groups are the other half. The identity provider states which groups you belong to, and applications use those for permissions. The trap is that applications disagree about the *shape* they expect — some want a bare group name, some want it qualified with an organisation. Emit the wrong shape and login succeeds while permissions silently do not, which reads as a broken app rather than a config error.
</Note>

## Where to go next

<CardGroup cols={2}>
<Card title="Secrets that expire" icon="clock-rotate-left" href="/explainers/secrets-that-expire">
How the machine credentials mentioned above are issued.
</Card>
<Card title="Security overview" icon="shield-halved" href="/security/overview">
The technical view of secrets handling across the stack.
</Card>
</CardGroup>
78 changes: 78 additions & 0 deletions explainers/overview.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
---
title: "Homelab explainers"
description: "Plain-language explanations of what a homelab is and how this one works — no prior infrastructure knowledge assumed."
page_type: explanation
---

> Everything else on this site assumes you already know what a hypervisor is. These pages don't.

A **homelab** is a small set of computers you run yourself, at home, to host the software you'd otherwise rent from someone else — photo storage, dashboards, a password manager, a media library, monitoring for your own network.

People build them for three reasons that usually arrive in this order: curiosity, then cost, then control. Curiosity gets you the first machine. Cost keeps you going when you notice how many monthly subscriptions one box replaces. Control is the one that lasts — your data stays on hardware you can physically touch, and nothing gets discontinued because a company changed direction.

The tradeoff is real and worth saying plainly: **you become the operations team.** When it breaks at 2am, nobody else is paged. Most of what's described in these pages exists to make that happen less often, and to make it recoverable when it does.

## Start here

<CardGroup cols={2}>
<Card title="What a hypervisor is" icon="layer-group" href="/explainers/what-is-a-hypervisor">
How one physical machine safely pretends to be many separate ones.
</Card>
<Card title="Clusters and quorum" icon="circle-nodes" href="/explainers/clusters-and-quorum">
Why several machines are joined together, and why the count is odd.
</Card>
<Card title="Staying up when a machine dies" icon="heart-pulse" href="/explainers/staying-up-when-a-machine-dies">
Backup, replication and failover are three different things.
</Card>
<Card title="One login for everything" icon="key" href="/explainers/one-login-for-everything">
What single sign-on actually does, and why it's safer than it sounds.
</Card>
<Card title="Secrets that expire" icon="clock-rotate-left" href="/explainers/secrets-that-expire">
Why passwords here are minted on demand instead of stored.
</Card>
</CardGroup>

## The shape of it

Four layers, each depending on the one below. Most homelab problems are a layer confusion — a networking problem being debugged as an application problem, or vice versa.

{/* Shape: linear chain. 4 nodes, top to bottom dependency order. Boundary crossings: 0. */}
{/* Aspect ratio ~3:1 horizontal, within tolerance for a 4-node chain. */}

```mermaid
%%{init: {'theme':'base','look':'handDrawn','themeVariables':{'fontFamily':'Geist','fontSize':'14px','primaryColor':'#102937','primaryTextColor':'#F4EFE6','primaryBorderColor':'#4FB3A9','lineColor':'#4FB3A9','secondaryColor':'#0B1D2A','tertiaryColor':'#1A2A38','clusterBkg':'rgba(79,179,169,0.08)','clusterBorder':'#4FB3A9'}}}%%
flowchart LR
Hw([Physical machines])
Hv([Hypervisor])
Gst([Guests])
App([Applications])

Hw --> Hv
Hv --> Gst
Gst --> App

classDef host fill:#102937,stroke:#4FB3A9,stroke-width:2px,color:#F4EFE6;
classDef sink fill:#102937,stroke:#F4EFE6,stroke-width:2.5px,color:#F4EFE6;

class Hw,Hv,Gst host
class App sink

click Hv "/explainers/what-is-a-hypervisor" "One machine, many guests"
```

**Physical machines** are the actual computers. **The hypervisor** is the software that divides each one into isolated slices. **Guests** are those slices, each behaving like its own computer. **Applications** are what you actually wanted — the photo library, the dashboard.

<Note>
Throughout these pages, internal names appear as placeholders — `example.local` for the internal domain, `node-a` and `node-b` for machines. That's deliberate: the concepts are the point, and publishing a real map of a private network isn't.
</Note>

## Where to go next

<CardGroup cols={2}>
<Card title="Infrastructure overview" icon="server" href="/infrastructure/overview">
The technical view of the same stack, once these pages make sense.
</Card>
<Card title="How it fits together" icon="diagram-project" href="/how-it-fits-together">
The six surfaces this site is organised around.
</Card>
</CardGroup>
97 changes: 97 additions & 0 deletions explainers/secrets-that-expire.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
---
title: "Secrets that expire"
description: "Why passwords and API keys here are issued on demand and die shortly after, instead of being stored in a file someone has to remember to rotate."
page_type: explanation
---

> A password stored in a file is a password you will still be finding in five years, in places you forgot you put it.

Automation needs credentials. A backup job needs the storage password, a deployment needs an API token. The obvious approach is to put them in a config file and lock it down.

That approach fails in a specific, predictable way, and it is not usually a dramatic breach. It is entropy.

## How stored credentials rot

They spread. A credential in a file gets copied into a second file for a second job, then into someone's notes to debug something at 1am, then into a CI variable. Nobody tracks the copies because each copy seemed reasonable at the time.

They outlive their purpose. The token issued for a one-off migration two years ago still works, still has full access, and nobody remembers it exists.

And rotation becomes frightening. Changing a credential means finding every copy, and since nobody knows where they all are, the rational choice is to leave it alone. So credentials get older and more widely spread, permanently.

## Issue on demand instead

Flip the model. Instead of storing credentials, store the *authority to create them*, and mint one when needed.

<Steps>
<Step title="Something needs access">
A job starts and needs to read from storage.
</Step>
<Step title="It proves what it is">
Not with the target credential, but with its own identity — one that says what role it plays, not what it may read.
</Step>
<Step title="A fresh credential is created">
The secrets manager generates a brand-new one, valid for this job, scoped to exactly what this role may do, and set to expire in an hour.
</Step>
<Step title="It expires">
The job finishes. Shortly after, the credential stops working — whether or not anyone remembered to clean it up.
</Step>
</Steps>

{/* Shape: linear chain, request through to expiry. 5 nodes, one gate. */}
{/* Boundary crossings: 0. The store is a cylinder; the policy check is the gate. */}

```mermaid
%%{init: {'theme':'base','look':'handDrawn','themeVariables':{'fontFamily':'Geist','fontSize':'14px','primaryColor':'#102937','primaryTextColor':'#F4EFE6','primaryBorderColor':'#4FB3A9','lineColor':'#4FB3A9','secondaryColor':'#0B1D2A','tertiaryColor':'#1A2A38','clusterBkg':'rgba(79,179,169,0.08)','clusterBorder':'#4FB3A9'}}}%%
flowchart LR
Job([Automation job])
Check{Allowed?}
Store[(Secrets manager)]
Cred([Short-lived credential])
Gone((Expires))

Job --> Check
Check --> Store
Store --> Cred
Cred --> Gone

classDef host fill:#102937,stroke:#4FB3A9,stroke-width:2px,color:#F4EFE6;
classDef gate fill:#102937,stroke:#E06B4A,stroke-width:2.5px,color:#F4EFE6;
classDef sink fill:#102937,stroke:#F4EFE6,stroke-width:2.5px,color:#F4EFE6;
classDef external fill:#102937,stroke:#E6B35A,stroke-width:2px,color:#F4EFE6;

class Job,Cred host
class Check gate
class Store sink
class Gone external
```

## What this buys

**Leaks have a deadline.** A credential found in a log tomorrow expired an hour after it was issued. This does not make a leak fine — it makes it survivable.

**Rotation stops being an event.** Every credential is new. There is no rotation project because there is nothing old to rotate.

**Access becomes visible.** Every issue is logged: which identity, which path, when. "Who read this?" becomes a query rather than an investigation.

**Revocation is real.** Remove a role's permission and the next request fails. You are not hunting copies, because there are no copies to hunt.

## Where it bottoms out

Something has to be trusted first. The job needs *some* credential to prove its identity, and that one cannot itself be issued on demand — this is the bootstrapping problem, and it does not fully disappear.

What it does is shrink. Instead of dozens of long-lived secrets spread across dozens of files, there is one small starting credential, and everything else derives from it. One thing to protect carefully is a much better position than fifty.

<Note>
The property worth insisting on is that a helper which cannot obtain a credential must **fail loudly**. A script that exits successfully having quietly exported nothing produces a confusing failure much later, in a different system, with no connection to the real cause. Silence is the expensive outcome, not the error.
</Note>

## Where to go next

<CardGroup cols={2}>
<Card title="One login for everything" icon="key" href="/explainers/one-login-for-everything">
The human-facing half of the same problem.
</Card>
<Card title="Security overview" icon="shield-halved" href="/security/overview">
The concrete tooling this describes in the abstract.
</Card>
</CardGroup>
Loading
Loading