# Autentizace před službami

Spousta self-hosted aplikací má přihlašování slabé nebo žádné. Řešením je postavit před ně vrstvu, která ověří uživatele dřív, než ho k aplikaci vůbec pustí.

Předpokládá to fungující [reverse proxy](Reverse-proxy).

Jestli si chceš jen vybrat a nečíst celou stránku: pro rychlé zakrytí jednoho dashboardu stačí [základní autentizace](#nejjednodušší-základní-http-autentizace), pro všechno ostatní vezmi [Pocket ID s tinyauthem](#doporučeno-pocket-id--tinyauth).

## Kdy to potřebuješ

**Aplikace nemá přihlašování vůbec.** Uptime Kuma na začátku, různé dashboardy, Prometheus, přehledy.

**Má, ale slabé.** Bez dvoufaktoru, bez omezení pokusů, s výchozím heslem.

**Chceš jedno přihlášení pro všechno.** Deset služeb, deset hesel je otrava a vede k opakovaným heslům.

Když je služba jen za [VPN](WireGuard), většinou to nepotřebuješ. Tohle je pro věci vystavené do internetu.

## Nejjednodušší: základní HTTP autentizace

Jeden řádek konfigurace, žádná další služba.

V [Caddy](Caddy):

```
tajne.example.cz {
    basic_auth {
        martin $2a$14$hash
    }
    reverse_proxy 127.0.0.1:9000
}
```

V [nginxu](Nginx):

```nginx
location / {
    auth_basic "Přihlášení";
    auth_basic_user_file /etc/nginx/.htpasswd;
    proxy_pass http://127.0.0.1:9000;
}
```

Omezení jsou reálná: ošklivé okno prohlížeče, žádný dvoufaktor, žádné odhlášení, rozbíjí to mobilní aplikace a API. Pro rychlé zakrytí dashboardu to stačí, pro cokoliv, co používáš denně, ne.

## Doporučeno: Pocket ID + tinyauth

Tohle je dnes pro homelab nejrozumnější kombinace a je to varianta, kterou bych volil znovu.

Dělí si práci na dvě části, které dělá každá jeden nástroj pořádně:

**[Pocket ID](https://pocket-id.org/)** je poskytovatel identity. Spravuje uživatele a skupiny a mluví OIDC. Zvláštní je tím, že **přihlašování řeší výhradně passkeys** — žádná hesla, ani jako záložní možnost. To zní jako omezení, ale je to ta nejlepší vlastnost celé sestavy: co neexistuje, nejde uhodnout, vyzradit ani vyphishovat. Nové zařízení se přidává jednorázovým kódem z administrace.

**[tinyauth](https://tinyauth.app/)** je brána. Sedí u [reverse proxy](Reverse-proxy), ptá se jí na každý požadavek a neověřené posílá na přihlášení. Umí obecné OIDC, takže ho na Pocket ID napojíš.

Proč ne rovnou něco jednoho: Pocket ID sám o sobě je jen poskytovatel identity, forward auth neumí. A tinyauth sám o sobě umí jen pár uživatelů v konfiguraci. Dohromady dají jednotné přihlášení s passkeys a paměťově se vejdou do desítek megabajtů — [Authentik](#authentik) potřebuje řádově víc.

### Docker Compose

```yaml
services:
  pocket-id:
    image: ghcr.io/pocket-id/pocket-id:v2
    restart: unless-stopped
    environment:
      APP_URL: https://id.example.cz
      ENCRYPTION_KEY: ${ENCRYPTION_KEY}            # povinné, min. 16 znaků
      TRUST_PROXY: "true"
      ALLOW_INSECURE_CALLBACK_URLS: "false"
    volumes:
      - ./pocket-id:/app/data
    networks: [web]

  tinyauth:
    image: ghcr.io/tinyauthapp/tinyauth:v5
    restart: unless-stopped
    environment:
      TINYAUTH_APPURL: https://auth.example.cz
      TINYAUTH_OAUTH_PROVIDERS_POCKETID_NAME: Pocket ID
      TINYAUTH_OAUTH_PROVIDERS_POCKETID_CLIENTID: ${OIDC_ID}
      TINYAUTH_OAUTH_PROVIDERS_POCKETID_CLIENTSECRET: ${OIDC_SECRET}
      TINYAUTH_OAUTH_PROVIDERS_POCKETID_AUTHURL: https://id.example.cz/authorize
      TINYAUTH_OAUTH_PROVIDERS_POCKETID_TOKENURL: https://id.example.cz/api/oidc/token
      TINYAUTH_OAUTH_PROVIDERS_POCKETID_USERINFOURL: https://id.example.cz/api/oidc/userinfo
      TINYAUTH_OAUTH_PROVIDERS_POCKETID_REDIRECTURL: https://auth.example.cz/api/oauth/callback/pocketid
      TINYAUTH_OAUTH_PROVIDERS_POCKETID_SCOPES: openid email profile groups
      TINYAUTH_OAUTH_AUTOREDIRECT: pocketid
    networks: [web]
```

**Hlídej si cestu k obrazu tinyauthu.** Původní `ghcr.io/steveiliop56/tinyauth` je zamrzlá na verzi 5.0.7 — projekt se přestěhoval pod organizaci a novější vydání včetně bezpečnostních oprav vycházejí jen jako `ghcr.io/tinyauthapp/tinyauth`. Spousta návodů má ještě starou cestu a `docker compose pull` na ní aktualizaci nenajde, protože tam žádná novější není.

Obě služby postav za proxy jako každou jinou, Pocket ID uvnitř poslouchá na portu `1411`.

**Pocket ID musí běžet přes HTTPS.** Passkeys stojí na WebAuthn a to funguje jen v zabezpečeném kontextu. Na `http://` se nepřihlásíš ani na zkoušku.

Pár proměnných, na kterých se to láme:

- `ENCRYPTION_KEY` je od verze 2 **povinný**. Bez něj Pocket ID nenastartuje.
- `TINYAUTH_SECRET` do konfigurace **nepatří** — od verze 4 neexistuje a tiše se zahodí. Relace jsou v databázi a cookie nese jen neprůhledné číslo. Ve starších návodech ho najdeš, ignoruj to. Totéž platí pro `TINYAUTH_PORT` (dnes `TINYAUTH_SERVER_PORT`) a `TINYAUTH_USERS` (dnes `TINYAUTH_AUTH_USERS`).
- `TINYAUTH_OAUTH_AUTOREDIRECT: pocketid` přeskočí přihlašovací stránku tinyauthu a pošle uživatele rovnou na Pocket ID. Když máš jen jednoho poskytovatele, chceš to.

### Propojení

V administraci Pocket ID založ OIDC klienta a jako návratovou adresu (callback) mu dej přesně tu z `REDIRECTURL` výše. Tvar je vždycky `<TINYAUTH_APPURL>/api/oauth/callback/<jméno-poskytovatele>`, tedy `pocketid` musí sedět s tím, co máš v názvech proměnných. Vygenerovaný identifikátor a tajemství doplň do `OIDC_ID` a `OIDC_SECRET`.

**Nově založený klient nepustí nikoho.** Tohle je past, na které skončí většina lidí s dojmem, že něco špatně opsali: čerstvý OIDC klient nemá povolenou žádnou skupinu, takže se přes něj nepřihlásí ani administrátor. V detailu klienta buď klikni na **Unrestrict**, nebo mu přiřaď povolené skupiny uživatelů.

### První přihlášení

Passkey si musí každý uživatel založit sám, admin to za něj udělat nemůže. Postup je takový, že mu v administraci vygeneruješ **jednorázový kód** (Users → login code, nebo `docker compose exec pocket-id /app/pocket-id one-time-access-token <uživatel>`), on ho zadá a při té příležitosti si passkey zaregistruje.

Stejnou cestou se řeší ztracený telefon. **Počítej s tím, že jsi trvale poslední záchrana** — kdo přijde o všechny passkeys, potřebuje tebe. Posílání kódů e-mailem sice jde zapnout, ale dokumentace to sama označuje za nebezpečné, protože pak stačí přístup do schránky.

### Napojení na proxy

tinyauth má pro každou proxy vlastní koncový bod: `/api/auth/traefik`, `/api/auth/caddy`, `/api/auth/nginx` a `/api/auth/envoy`. Jiné jméno vrátí `400`.

[Caddy](Caddy) — vytvoř si úryvek a použij ho tam, kde chceš:

```
(chraneno) {
    forward_auth tinyauth:3000 {
        uri /api/auth/caddy
        copy_headers Remote-User Remote-Name Remote-Email Remote-Groups
    }
}

nas.example.cz {
    import chraneno
    reverse_proxy 127.0.0.1:5000
}
```

Nic víc není potřeba. Caddy vrátí klientovi neúspěšnou odpověď tinyauthu i s přesměrováním, takže žádný `handle_response` nepiš — přidáním si to jen rozbiješ.

[Traefik](Traefik):

```yaml
labels:
  - traefik.http.middlewares.tinyauth.forwardauth.address=http://tinyauth:3000/api/auth/traefik
  - traefik.http.middlewares.tinyauth.forwardauth.authResponseHeaders=Remote-User,Remote-Name,Remote-Email,Remote-Groups
```

`authResponseHeaders` uveď vždycky, jinak se identita k aplikaci nedostane. `trustForwardHeader` naopak nenastavuj — Traefik si hlavičky `X-Forwarded-*` vyrábí sám. Potřebuješ ho jen tehdy, když je Traefik sám za další proxy.

[Nginx](Nginx):

```nginx
location / {
    auth_request /tinyauth;
    auth_request_set $user  $upstream_http_remote_user;
    auth_request_set $cil   $upstream_http_x_tinyauth_location;
    proxy_set_header Remote-User $user;
    error_page 401 403 = @prihlasit;
    proxy_pass http://127.0.0.1:5000;
}

location = /tinyauth {
    internal;
    proxy_pass http://tinyauth:3000/api/auth/nginx;
    proxy_set_header X-Forwarded-Proto $scheme;
    proxy_set_header X-Forwarded-Host  $http_host;
    proxy_set_header X-Forwarded-Uri   $request_uri;
    proxy_pass_request_body off;
    proxy_set_header Content-Length "";
}

location @prihlasit {
    return 302 $cil;
}
```

**Přihlašovací adresu si nesestavuj sám.** tinyauth ji pošle hotovou v hlavičce `x-tinyauth-location` a ty ji jen převezmeš. Návody, které tu adresu skládají ručně z parametrů, se rozbijí při první aktualizaci.

### Kdo smí kam

Tady je past, na které to lidem tiše nefunguje, jak si myslí.

**Kde se ověřuje, určuje proxy. Kdo smí projít, určuje tinyauth.** To jsou dvě nezávislé věci. Když někam přidáš middleware, zapnul jsi ověření totožnosti — ale ve výchozím nastavení je politika `allow`, takže aplikace bez vlastního pravidla pustí **kohokoliv přihlášeného**. Pro rodinný Jellyfin to je v pořádku, pro rozhraní hypervizoru ne.

Omezení na konkrétní lidi nebo skupiny z Pocket ID se nastavuje v tinyauthu per aplikaci a aplikace se páruje podle hlavičky `Host`. Buď jí nastavíš `config.domain`, nebo se jméno aplikace musí rovnat první části doménového jména (`nas` ↔ `nas.example.cz`).

Omezení podle IP adresy (`ip.allow`, `ip.block`) navíc **mlčky nedělá nic**, dokud nenastavíš `TINYAUTH_AUTH_TRUSTEDPROXIES`. Bez toho tinyauth vidí jako zdroj adresu proxy, ne klienta.

### Doména cookie

Platí tu obecné pravidlo forward auth ze [schématu níž](#jak-forward-auth-funguje), ale tinyauth ho odvozuje po svém a stojí za to vědět jak: **vezme hostname z `TINYAUTH_APPURL` a odřízne první část.**

Z `auth.example.cz` tedy vznikne cookie pro `example.cz`, kterou dostanou všechny služby na `*.example.cz`. To chceš.

Dvě situace, kdy to nevyjde:

- **`auth.sluzby.example.cz`** vyrobí cookie jen pro `sluzby.example.cz`. Služba na `nas.example.cz` ji nikdy nedostane a skončíš v nekonečné smyčce přihlášení → aplikace → přihlášení.
- **Doména od dynamického DNS.** Když z `auth.neco.duckdns.org` vyjde `duckdns.org`, tinyauth **vůbec nenastartuje** a napíše, že doména je v seznamu veřejných přípon. Cookie pro `duckdns.org` by totiž dostal každý, kdo tam má subdoménu. Řešení je vlastní doména.

Když se ti přihlášení točí dokola, otevři v prohlížeči vývojářské nástroje a podívej se, jestli se cookie posílá i na doménu chráněné služby. Skoro vždycky je to tohle.

### Kdy tuhle kombinaci nebrat

Poctivě — nehodí se vždycky:

- **Potřebuješ SAML.** Neumí ho ani Pocket ID, ani tinyauth. (Pro pořádek: Authelia taky ne. SAML dnes reálně znamená [Authentik](#authentik) nebo Keycloak.)
- **Aplikace se potřebuje připojit k LDAP serveru.** Obě součásti umí LDAP jen číst — Pocket ID z něj synchronizuje uživatele, tinyauth proti němu umí ověřovat. Ani jedna ale není LDAP server, na který by se stará aplikace přihlásila. To je opět Authentik.
- **Někteří uživatelé passkeys nezvládnou.** Pocket ID záměrně nemá hesla vůbec. Když víš, že to u tebe neprojde, chceš [Authelii](#authelia).
- **Chceš podmíněný přístup** ve stylu „z domova stačí heslo, zvenku i druhý faktor", nebo pořádná role a oprávnění. tinyauth umí jen seznamy povolených a zakázaných; autor sám v dokumentaci píše, že na prostředí vyžadující RBAC nemá cílit.

## Authelia

Nejrozšířenější řešení a dlouho výchozí volba. Přihlašovací stránka, dvoufaktor, pravidla podle domény a cesty. Vezmi ji tam, kde potřebuješ hesla nebo pravidla podle sítě.

```yaml
services:
  authelia:
    image: authelia/authelia:latest
    volumes:
      - ./authelia:/config
    networks: [web]
    labels:
      - traefik.enable=true
      - traefik.http.routers.authelia.rule=Host(`auth.example.cz`)
      - traefik.http.middlewares.authelia.forwardauth.address=http://authelia:9091/api/verify?rd=https://auth.example.cz
      - traefik.http.middlewares.authelia.forwardauth.trustForwardHeader=true
      - traefik.http.middlewares.authelia.forwardauth.authResponseHeaders=Remote-User,Remote-Groups,Remote-Email
```

Chráněnou službu pak označíš:

```yaml
labels:
  - traefik.http.routers.app.middlewares=authelia
```

Pravidla v `configuration.yml`:

```yaml
access_control:
  default_policy: deny
  rules:
    - domain: "verejne.example.cz"
      policy: bypass

    - domain: "nas.example.cz"
      policy: one_factor
      networks: [192.168.1.0/24]

    - domain: "nas.example.cz"
      policy: two_factor

    - domain: "*.example.cz"
      policy: two_factor
      subject: ["group:admin"]
```

Ta dvojice pravidel pro `nas.example.cz` je užitečný vzor: **z domácí sítě stačí heslo, z internetu se vyžaduje i druhý faktor.** Pořadí rozhoduje, první shoda vyhrává.

Uživatelé můžou být v souboru (`users_database.yml`) nebo v LDAP. Pro domácnost stačí soubor.

## Authentik

Plnohodnotný poskytovatel identity. Umí OAuth2, OIDC, SAML, LDAP a proxy autentizaci.

Vezmi ho, když:

- máš aplikace, které umí OIDC a chceš skutečné jednotné přihlášení, ne jen bránu
- chceš uživatelské rozhraní pro správu uživatelů místo editace YAML
- potřebuješ propojit s Google nebo GitHub účty

Cenou je podstatně vyšší spotřeba paměti a složitější nastavení. Pro tři služby je to kanón na vrabce.

## Jak forward auth funguje

Užitečné vědět, protože se to špatně ladí naslepo. Schéma platí pro všechny brány, jména jsou z kombinace výše:

```mermaid
sequenceDiagram
    participant K as Klient
    participant P as Proxy
    participant T as tinyauth
    participant I as Pocket ID
    participant S as Služba
    K->>P: GET nas.example.cz
    P->>T: mám ho pustit?
    T->>P: ne, neověřený
    P->>K: přesměrování na přihlášení
    K->>I: přihlášení passkeyem
    I->>T: totožnost a skupiny (OIDC)
    T->>K: cookie pro example.cz
    K->>P: GET nas.example.cz (s cookie)
    P->>T: mám ho pustit?
    T->>P: ano, je to martin
    P->>S: GET / + hlavička Remote-User
    S->>K: obsah
```

Z toho plynou dvě věci, na kterých se to nejčastěji láme.

**Cookie musí platit pro celou doménu.** Proto musí být brána na subdoméně stejné domény jako chráněné služby. `auth.example.cz` a `nas.example.cz` ano, `auth.jina.cz` a `nas.example.cz` ne. Jak přesně tu doménu odvozuje tinyauth je [výš](#doména-cookie).

**Proxy se ptá při každém požadavku.** Když je brána dole, nedostaneš se nikam. Počítej s tím a nedávej za ni věci, které potřebuješ, když se něco pokazí — třeba monitoring nebo rozhraní hypervizoru.

## Předávání identity aplikaci

Některé aplikace umí důvěřovat hlavičce `Remote-User` a přihlásit uživatele automaticky. Nextcloud, Grafana a Jellyfin to zvládají.

tinyauth posílá dál pětici `Remote-User`, `Remote-Name`, `Remote-Email`, `Remote-Groups` a `Remote-Sub`. Dvě z nich jsou podmíněné: `Remote-Groups` dostaneš jen u přihlášení přes OIDC nebo LDAP, `Remote-Sub` jen u OIDC. Kdo se přihlásí lokálním účtem tinyauthu, přijde k aplikaci **bez skupin** — a aplikace, která podle nich přiděluje oprávnění, ho pak nikam nepustí.

Je to pohodlné, ale **nebezpečné, pokud jde aplikace obejít**. Kdyby se někdo dostal na aplikaci mimo proxy a poslal si `Remote-User: admin`, je uvnitř. Proto musí služba poslouchat jen na loopback a být dostupná výhradně přes proxy — viz [Docker a firewall](Docker-a-firewall).

## Co za autentizaci nedávat

**Cokoliv, co používá mobilní aplikace nebo API.** Forward auth je postavená na cookies a přesměrování v prohlížeči. Immich, Nextcloud a podobné mají vlastní přihlašování — nech ho a autentizaci před ně nedávej, jinak si rozbiješ synchronizaci.

**Věci, které potřebuješ, když se to rozbije.** Monitoring, konzole hypervizoru, IPMI. Ty dej za [VPN](WireGuard).

**Webhooky a callbacky.** Zaplacení, notifikace z externích služeb. Musí mít výjimku (`policy: bypass`) na konkrétní cestu.

## Časté chyby

| Problém | Příčina |
|---|---|
| Přihlášení se točí dokola | cookie neplatí pro doménu služby — viz [doména cookie](#doména-cookie) |
| tinyauth vůbec nenastartuje | odvozená doména je veřejná přípona, typicky dynamické DNS |
| Přihlásí se kdokoliv, i když jsi nastavil skupiny | výchozí politika je `allow`, aplikace bez pravidla pustí každého |
| Pocket ID nepustí ani tebe | nový OIDC klient nemá povolenou žádnou skupinu, chybí **Unrestrict** |
| Passkey nejde zaregistrovat | Pocket ID neběží přes HTTPS |
| Aplikace nevidí uživatele | u Traefiku chybí `authResponseHeaders` |
| Omezení podle IP nefunguje | není nastavené `TINYAUTH_AUTH_TRUSTEDPROXIES` |
| Aktualizace nepřicházejí | starý obraz `steveiliop56/tinyauth`, přejdi na `tinyauthapp/tinyauth` |
| Rozbila se mobilní aplikace | forward auth nepatří před služby s vlastním klientem |
