Clone this repository
For self-hosted knots, clone URLs may differ based on your setup.
Incident 2026-10-17: radio→styx UDP was blackholed for ~40 min after a
sendto() hit ENETUNREACH; tincd never recovered the UDP path on its own.
The fix is upstream as Mic92/tincr#140.
The fork is rebased onto upstream Mic92/tincr main: 6 of the 8 commits on
lassulus/fixes are upstream now, leaving the tunnel-address gate and the
ENETUNREACH fix on lassulus/fixes-rebased (bfcbbc69).
pkgs/tincr pins the same branch and is what every machine runs (the flake
input only provides barnacle's darwin module), so bump it and its update
script along with the input.
RUST_LOG=tinc=warn hid every routing/relay event, so path problems were
only visible over SSH. Enable Info for tincd::graph (became
reachable/unreachable via X), tincd::conn (meta connections up/down) and
tincd::net (key exchange, UDP idle revalidation/timeouts, PMTU
fixed/increase/decrease). Their Debug/Trace stays off because tincd::net
and tincd::proto log per packet there; tincd (autoconnect), tincd::auth
and tincd::proto Info stay off as redundant.
Expected volume is roughly 100-250 lines/h on a hub like neoprism and
30-60/h on leaves, plus autoconnect dial attempts to dead hosts in conn.
iets (numtide/iets, a Nix evaluator in Rust) gives the same drvPath,
isCached and cacheStatus for every machine as nix-eval-jobs 2.35.4, in
24-35s instead of 108-144s (4 workers, coaxmetal). It is a flake input
so CI builds it once and pushes it to cache.lassul.us with niks3 and
the lazy store.
Reading the lock file from the fetched src is import from derivation:
every evaluation of the machines that have it (aergia, green, ignavia,
mors) fetches the source first, and evaluators without IFD (iets) fail
on them.
Account linking is off: byoidc never marks emails verified and anyone can
run a provider claiming any address. New users are created automatically.
idps.<name>.type = "oidc" with an issuer, a literal clientId as
alternative to clientIdFile, a display name independent of the attribute
name and per-IdP providerOptions (default unchanged: linking, creation and
auto update allowed). Generic OIDC IdPs use PKCE and ID token mapping.
Zitadel 2.71 does not fetch the issuer's discovery document when the IdP
is created, only at login, so the reconciler needs no ordering on the
provider.
byoidc lets people sign in with their own OpenID provider; Zitadel will
offer it as one generic OIDC IdP. The downstream client "zitadel" gets its
secret from a prompt-less clan var, which root-owned is fine for both
byoidc (LoadCredential) and zitadel-reconcile (root).
Its state (cached upstream clients, signing key) is not backed up: subjects
are derived from issuer and upstream sub, a new signing key is picked up
through JWKS, and upstream clients are registered again on demand.
id.lassul.us is Zitadel now; its endpoints are /oauth/v2/authorize,
/oauth/v2/token and /oidc/v1/userinfo (from its discovery document). The
pad project is public: every registered user may log in. zitadel-reconcile
creates the HedgeDoc app and writes its client id/secret into the env file
HedgeDoc reads, so there is no prompt var for them; hedgedoc starts after
it. HedgeDoc posts the client secret in the token request body, hence the
POST auth method.
lass.zitadel describes Zitadel's login policy, its external IdPs and one
project per service with OIDC apps and an access mode:
- public: no "check authorization on authentication", every registered
user may log in.
- declarative: the check is on and the project's user grants are exactly
the listed users (login name or verified email address). Removing a
user from the list revokes the grant; grants given in the console are
removed as well.
- managed: the check is on, the grants are left to the console.
zitadel-reconcile, a oneshot after zitadel, applies it at boot and on
every deploy that changes it. It authenticates with the PAT of a service
user (IAM_OWNER) that zitadel's setup creates with the first instance
(FirstInstance.Org.Machine, PatPath). Setup writes that file world
readable, so /var/lib/zitadel becomes a 0700 state directory. An empty Pat
section creates no token, hence the explicit expiration date (Zitadel's
own default).
A small Python script against the admin/management v1 REST APIs rather
than OpenTofu with the zitadel provider:
- "declarative" has to own all grants of a project, including ones made
in the console; terraform only removes what is in its state.
- Users are listed before they register. A failed user lookup would fail
the whole plan; the script skips such users until the next run.
- No state file that has to match Zitadel: objects are found by name, so
e.g. an app deleted in the console is created again.
- The provider talks gRPC to the external domain, through nginx and TLS;
the script talks plain HTTP to 127.0.0.1 with the Host header.
- Projects and apps removed from the config stay in Zitadel instead of
being destroyed together with their grants.
Zitadel shows a client secret only when it creates one. The script keeps a
root-only copy per app in /var/lib/zitadel-apps and renders the app's env
file from it (path, owner and variable names configurable); without a copy
for the current client id it generates a new secret. Services on other
hosts find their secret there too, for now.
The login policy moves from DefaultInstance, which only applies when the
instance is created, to lass.zitadel.loginPolicy. GitHub and Google logins
come from persisted prompt vars; an empty prompt leaves the IdP out, so
the first deploy works before the OAuth apps exist. Their callback is
/ui/login/login/externalidp/callback (login UI v1, the default in 2.71).
Users may create accounts with them and link them to existing ones.
The script waits for /admin/v1/healthz rather than /debug/ready: the HTTP
gateway's connection to the gRPC API comes up later.