Table of Contents
- Cairn — Project Wiki
- Status
- Key decisions (with reasoning)
- Runner / CI lessons (IMPORTANT — applies to all repos on this Forgejo)
- Coordination gotcha: parallel sessions racing the same task (2026-07-20)
- Tooling gotcha: Claude Desktop MCP "server hang" that isn't (2026-07-19)
- Content round-trip gotcha (2026-07-21)
- Image gotcha: Bitnami Docker Hub deprecation (2026-07-21)
- Known limitations (tracked as issues)
- Test infrastructure (homelab)
- Development notes
- Roadmap snapshot
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
2026-07-25: encryption reframed as an open fork + post-quantum correction. (Also re-records the encryption status — an earlier wiki entry was lost to a
master/mainbranch mixup in the docs-patch workflow.) The age at-rest encryption probe is shipped and verified live (v0.2.3–v0.2.4):internal/storage/encryptage decorator aroundstorage.Driver, per-user keys in the Keycloak profile (cairnAgeIdentity, keyCustodykeycloak-profile), encrypt-to-[user, recovery], transparent to REST/WebDAV; content-only; server-side (not client E2E). Correction (verified against age README +pq.go): age IS post-quantum-capable — v1.3.0+ ships hybrid ML-KEM-768 + X25519 (age-keygen -pq; the Go library exportsGenerateHybridIdentity/ParseHybridIdentity/ParseHybridRecipient), and we pin v1.3.1, so PQ is a config toggle, not a new stack. We ship classical X25519 today; the earlier "not post-quantum" note was wrong. Constraint: a Hybrid recipient can't mix with classical, so the recovery recipient must also be Hybrid. Open fork (Manuel, 2026-07-24): key custody splits into two orthogonal questions — (A) where keys live uniformly across Keycloak+AD (Keycloak profile today; OpenBao transit as the uniform option; AD unresolved), and (B) can the backend avoid holding a usable key (Manuel'spasswordHash + PINderivation — RAM-only, zeroed after use — which works for local/LDAP but is impossible under OIDC/app-passwords; the E2E trade relocated). OpenBao answers A, not B. Also on the table: demote the single deployment key + add a per-instance seed (blast radius); group dirs via age native multi-recipient; a separate--target=cryptorole for independent crypto scaling (Manuel's actual reason for a separate service); and a Datentresor (encrypted-vault folder) fallback if whole-backend transparent encryption proves too heavy. Decision record:ENCRYPTION-COMPARISON.md(rewritten as the open fork). Issues #15, #28–#33. Two independent prototypes (Manuel's + an isolated spike of ours) feed the decision before we commit — the OIDC-fork playbook. PQ + custody are v0.4; quota enforcement (#14) + seekable ranged reads (#15) finish v0.3.
2026-07-24 (later): consolidated to a single canonical dogfood. Trimmed the three-instance dogfood setup to one:
cairn-encat.249(S3/Garage bucketcairn-enc+ OIDC/Keycloak + age encryption, keyCustodykeycloak-profile) is now the only running Cairn — the "actual product" config, ahead of external exposure. Retiredcairn(.247, POSIX/NFS, local auth) andcairn-s3(.250, S3 plaintext): manifests removed from homelab-configk3s/cairn/(deployment.yaml/service.yaml/storage.yaml/s3-dogfood.yaml) and the live objectskubectl deleted (Argo prune is off, so removing from git alone does not reap them). Thecairn-dataNFS PV wasRetain, soDaPool/cairn-dataon TrueNAS is left intact (delete there to reclaim space). ServiceMonitorcairnrepointed fromname=cairntopart-of=cairnsoup{namespace="cairn"}(and theCairnDown/CairnScrapeMissingalerts) keeps tracking the surviving instance. The coverage the retired instances added (POSIX ranged reads; S3-plaintext ranged reads — note the encrypted path currently does full-object reads) now lives only in unit/conformance tests + the on-demandinterop-s3.yml/interop-enc.ymlworkflows; re-spin from the kept manifests if a standing instance is ever needed again. Deferred cosmetic: the survivor is still namedcairn-encat.249— renaming to plaincairn(and moving to.247or straight tofiles.c0rdyceps.ch) folds into the external-exposure step, which touches OIDC redirect URIs / Caddy anyway. NOTE: the "Test infrastructure" and older "Status" sections below predate this consolidation and still list three instances — this entry supersedes them.
2026-07-21: S3 driver verified as a live dogfood against Garage. Stood up a second dogfood instance
cairn-s3at.250(storage.driver: s3,v0.2.1,auth.mode: local) backed by a live Garage bucket (cairn-dogfoodon the TrueNAS Garage at192.168.10.32:3900), and raninterop-s3.yml: WebDAV over the hand-rolled SigV4 client — PROPFIND 207, wrong-password 401, MKCOL 201 (writes the S3 directory-marker object), PUT 201, GET byte-match, PROPFIND Depth 1 lists the object, DELETE file 204, DELETE dir 204 (recursive prefix delete). The previously env-gated conformance suite (SeaweedFS) now has a live-deployment proof against the actual dogfood target (Garage). Config lives in acairn-s3-configk8s secret (endpoint/bucket/region + Garage key + the reusednikolapasswordHash, so the existingCAIRN_TEST_PASSWORDactions secret works unchanged);/datais emptyDir scratch for the TUS spool only (interrupted uploads don't survive a restart on this instance — fine for a test dogfood). Closes ARCHITECTURE.md §9's "a second instance on the S3 driver against Garage remains planned". Garage setup wasdocker execinto the Garage container:garage bucket create+key create+bucket allow --read --write; region must match Garage'ss3_region(defaultgarage) or SigV4 403s.
2026-07-21: v0.3 underway — quota (reporting + WebDAV props), admin API. Three v0.3 code increments merged via
feat/*→ PR → squash: quota reporting (GET /api/v1/quota, feature-detectedstorage.Usager; POSIX statfs, S3 → 501; PR #8), WebDAV RFC 4331 quota props (D:quota-available-bytes/D:quota-used-byteson collections so Finder shows free space; PR #9), and the admin API (auth.adminsusername allow-list working across local/OIDC/LDAP;GET /admin/info,GET /admin/shares= all links,DELETE /admin/shares/{token}= revoke any; PR #10). Quota reporting + WebDAV props shipped in release v0.2.1 and the.247dogfood was rolled to it (Finder PROPFIND 207 confirmed serving the props live). Workflow note: minor/patch tags are now cut autonomously (owner-approved); CI runs onpull_request(not branch push).
2026-07-21: Active Directory live-bind verified — LDAP fully proven. Stood up a throwaway Samba AD DC on k3s (
k3s/samba-ad/, plain Debian + boot-timesamba-tool domain provision, arm64) + acairn-ad-testinstance at.250(auth.mode: ldap, LDAPS tosamba.samba-ad.svc:636,ldapUserFilter: (sAMAccountName=%s),ldapInsecureSkipVerifyfor the self-signed cert), and raninterop-ad.yml: WebDAV Basic as the AD userldaptest→ search-then-bind over LDAPS against live Samba AD → PROPFIND 207, PUT 201, GET byte-match, DELETE 204; wrong-password and unknown-user both 401. Both the OpenLDAPuidand the Active DirectorysAMAccountName/LDAPS paths are now live-verified — AD support is proven, not merely claimed. Samba-on-arm64-in-k8s gotchas (all fixed, in order): domain provision needs thesamba-ad-provision(schema LDIFs),samba-dsdb-modules(samba_secretsLDB module) andsamba-vfs-modules(acl_xattr) packages; writing the sysvolsecurity.NTACLxattr needsCAP_SYS_ADMIN; the AD DC forks/usr/sbin/winbinddat startup so thewinbindpackage must be installed too (else "Failed to exec child" → exit 255); and because the container rootfs is ephemeral while the domain DB lives on anemptyDir, the provisioned/etc/samba/smb.confmust be persisted into the volume and restored on restart — otherwise a reinstalled container boots with the defaultserver role = standaloneand exits with "misconfigured server role" (error 22) while the provision marker suppresses re-provisioning. Torn down after:k3s/samba-ad/+k3s/cairn/test-instance-ad.yamldeleted from git;kubectl delete ns samba-ad+ thecairn-ad-testobjects + secrets.
2026-07-20 (night): v0.2 milestone COMPLETE. All three remaining v0.2 features landed on main, CI green (run 174, commit
8a091e0): LDAP provider (internal/auth/ldapauth, go-ldap search-then-bind,auth.mode: ldap, filter-escaping + tests), TUS 1.0.0 resumable uploads (internal/tusat/api/v1/tus/, spool + JSON sidecar surviving restarts), and the app-password management UI (feature-detected panel in the embedded web UI, mirrors the shares UI). Earlier today the OIDC real-IdP e2e PASSED against Keycloak on k3s (browser Code+PKCE login →/api/v1/me={"username":"nikola-test"}; app-password WebDAV PROPFIND 207 / PUT 201 / GET 200 / wrong-secret 401 / DELETE 204; pod 0 restarts). Keycloak lives at192.168.10.248(homelab-configk3s/keycloak/), Cairn OIDC test instancev0.0.3at192.168.10.249.
2026-07-21: TUS real-client verified. Cut release v0.2.0 (ko multi-arch, run 174→image), rolled the dogfood at
.247to it, and ran the newinterop-tus.yml(manual dispatch, LAN):eventials/go-tuscreated a 12 MiB upload in 2 MiB chunks, was interrupted at 4 MiB, resumed from the server's HEAD-reported offset (exactly 4194304), finished, and the stored file was byte-identical (then DELETE 204).
2026-07-21: LDAP live-bind verified — v0.2 fully verified. Deployed a throwaway OpenLDAP (
k3s/openldap/,bitnamilegacy/openldap, declaratively seededuid=ldaptest) + acairn-ldap-testinstance at.250(auth.mode: ldap, search-then-bind againstldap.openldap.svc), and raninterop-ldap.yml: WebDAV Basic asldaptest→ search-then-bind against live OpenLDAP → PROPFIND 207, PUT 201, GET byte-match, DELETE 204; wrong-password and unknown-user both 401. Every v0.2 feature now has a real-world verification. Two gotchas hit and fixed: (1) Bitnami emptied its Docker Hubbitnami/images in the 2025 deprecation (bitnami/openldap:latest404s) → usebitnamilegacy/openldap; (2) the deprecatedLDAP_USER_DC+LDAP_GROUPenv collided onou=users, so tree-seeding added it twice → exit 68 (LDAP entryAlreadyExists) → CrashLoopBackOff → drop both (users default toou=usersanyway).
Cairn — Project Wiki
What: Self-hosted file storage, single Go binary. Web UI + WebDAV + pluggable storage (POSIX/S3) + pluggable identity (OIDC + LDAP implemented). "A better Nextcloud without the bloat" — see ARCHITECTURE.md in the repo for the full design; this wiki holds the working log, decisions, and gotchas.
Status
As of 2026-07-21: v0.2 complete + fully verified; v0.3 well underway. OIDC (Keycloak e2e), app passwords (API + UI), share links, TUS (go-tus resume-from-offset), and LDAP (live-bind against OpenLDAP uid and Samba AD sAMAccountName/LDAPS) all proven against live deployments. v0.3 so far: quota reporting (GET /api/v1/quota) + WebDAV quota props (RFC 4331) shipped in v0.2.1 (dogfood .247 rolled to it, Finder shows free space); admin API (auth.admins, merged); S3 driver verified as a live dogfood against Garage (cairn-s3 at .250). Remaining v0.3: age-encrypted storage (§4.3) — the last planned item; wants a key-custody + ranged-reads design pass before implementation. Optional follow-ups still open: quota enforcement, a real Uppy/tus-js browser upload, LDAP group resolution.
As of 2026-07-20 (evening): OIDC decision resolved and merged. External expert review chose the library route; PR #7 merged oidc-lib (through oidc-common) into main and both prototype branches were deleted — the hand-rolled verifier survives in git history, and OIDC-COMPARISON.md on main is the decision record. Main now has: OIDC Code+PKCE via go-oidc/v3+x/oauth2 (auth.mode: "oidc"), memory sessions + /auth/* endpoints, app passwords (atomic-JSON store, prefix-gated Basic provider, /api/v1/app-passwords), and the issue-#5 OOM limiter refactored to protect all argon2 verifiers. Dependency count: 4 direct-ish (x/crypto, go-oidc/v3, x/oauth2, + transitive go-jose/v4/x/sys); Go floor now 1.25 (go-oidc requirement). Also: Helm chart at deploy/chart (CI-validated), homelab validate.yml replaced with kubeconform, and the Forgejo→Argo webhook fixed (had never worked; two stacked faults — see homelab docs).
As of 2026-07-20 (morning): Issue #5 root-caused and fixed, v0.0.2 live on k3s, client interop (#2) complete. The Finder-mount pod crashes were OOM kills: each argon2id verification allocates its full 64 MiB parameter and Local.Authenticate ran them unbounded — Finder's parallel mount connections (identical credentials, cold cache) × 64 MiB blew the 256Mi limit. Fix (PR #6): inflight dedupe of identical concurrent verifications + a semaphore capping concurrent argon2 runs at 2 (~128 MiB worst case). Hardening: IdleTimeout 120s, outermost slog panic-recovery middleware, first internal/dav test batch. Interop matrix: Finder ✔, rclone ✔, Cyberduck ✔ — pod 0 restarts, ~132Mi peak; details on issue #2.
As of 2026-07-19: v0.0 scaffold ✔ · argon2id auth (#1) ✔ · observability + graceful shutdown (#4) ✔ · S3 driver ✔ (hand-rolled SigV4 + minimal client, conformance-tested against a live SeaweedFS gateway) · share links ✔ (file-backed store, public /s/ surface with passwords/expiry/file-drop) · share links web UI ✔ (feature-detected). Full end-to-end smoke tests after every feature.
Key decisions (with reasoning)
| Decision | Why |
|---|---|
| Go over Rust | Single static binary, trivial arm64/amd64 cross-compile, go:embed for the UI, larger contributor pool, faster iteration for an MVP. |
Hand-rolled WebDAV, not x/net/webdav |
That package's FileSystem contract assumes os.File semantics that map poorly to object stores; programming against our own storage.Driver keeps one abstraction. Also keeps the core dependency-minimal. |
| No database file index | The storage backend is the source of truth (anti-oc_filecache stance). Out-of-tree state exists only for share links and app passwords — state that genuinely can't live in the file tree. |
| argon2id for local auth | PHC string format, RFC 9106 second recommended parameters; cairnd hash-password reads stdin only; dummy-hash verification for unknown users closes the enumeration timing oracle; parameter validation prevents panic/memory-DoS from hostile hashes. |
| Verified-credentials cache in local/LDAP auth | HTTP Basic resends credentials per request → every request paid full argon2 cost (~800ms) / an LDAP bind. SHA-256 fingerprint cached only after the credential is accepted; failure timings unchanged. Once per session instead of per request. |
| Bounded + deduped argon2 verification (issue #5) | argon2id's 64 MiB-per-verification is a memory-amplification vector under parallel Basic-auth storms. Inflight map dedupes identical concurrent verifications to one argon2 run; a process-wide semaphore (2) caps distinct runs at ~128 MiB. Shared by local auth and app passwords via verifyLimiter. |
| OIDC via go-oidc + x/oauth2, not hand-rolled (decided 2026-07-20, expert-reviewed) | Both variants were fully built and CI-tested against an identical fake-IdP attack suite before deciding. Library route won: audited/standard implementations, token verification is where hand-rolled auth historically fails, and the trust-surface argument was already weakened by the accepted go-ldap/ldap dependency for LDAP. OIDC-COMPARISON.md = decision record. Real-IdP e2e passed against Keycloak 2026-07-20. |
| App passwords bridge WebDAV in OIDC mode | Sync clients can't do browser redirect flows. Per-device cairn--prefixed secrets, shown once, argon2id-hashed in an atomic-JSON store; prefix gating keeps non-app-password Basic attempts from burning argon2 in the wrong provider. Nextcloud/Vaultwarden pattern. LDAP mode doesn't need them (directory creds are Basic-compatible — verified 2026-07-21, OpenLDAP + Samba AD). |
| Hand-rolled SigV4 + minimal S3 client, no SDK | ~120 lines of well-specified HMAC chaining (verified against AWS's published test vector) vs an entire SDK dependency tree. Path-style URLs for Garage/MinIO/SeaweedFS. Verified as a live dogfood against Garage 2026-07-21 (interop-s3.yml). |
Share store: atomic-JSON file behind a Store interface, not SQLite (for now) |
modernc.org/sqlite is a transpiled-C codebase + libc emulation — the largest possible addition to the trust surface — for a write volume of "humans clicking buttons". Interface means SQLite can slot in later. Same pattern reused for app passwords. |
| Optional capabilities via interface + type assertion (not fatter Driver) | Features not every backend can serve (presigned URLs, now Usager quota reporting) are separate one-method interfaces the caller feature-detects, rather than bloating the core storage.Driver contract with methods half the drivers would stub. POSIX implements Usager via statfs; S3 doesn't, and GET /api/v1/quota returns 501 there. |
Admin identity = username allow-list (auth.admins), not a role system (yet) |
Cheapest thing that works across local/OIDC/LDAP (all yield a User.Username). Enables the minimal operator surface (/api/v1/admin/*) without per-file ACLs, which §5.4 deliberately defers. |
| Fake LOCK/UNLOCK | Finder and MS Office refuse to write without class-2 lock support. Tokens are granted and ignored. Revisit only if concurrent-edit corruption is reported. |
Depth: infinity PROPFIND rejected |
Unbounded tree walks are a DoS vector. Clients all use Depth 0/1 in practice. |
| Releases via ko from the runner, distroless base (not scratch) | The host-mode runner has no Docker daemon; ko builds Go images daemonlessly and pushes a proper multi-arch manifest. distroless/static over scratch: adds CA roots + tzdata — needed the moment OIDC talks to an HTTPS IdP. |
| Fair-code license (n8n SUL model), not OSI open source | Free ≤50 users, commercial above, no hosted resale. Never call it "open source" in marketing. Identity integrations (OIDC/LDAP) are Community-tier — no SSO tax; the 50-user line is the commercial fence, not capability gating. |
| License validation must be offline-capable | Ed25519-signed license files verified locally. Sovereignty/air-gapped customers are the target market. Licensing outage must never lock data access. |
| age-encrypted storage for untrusted backends (planned, §4.3) | External review proposed at-rest encryption; adopted as a storage.Driver decorator using filippo.io/age (library, not a service tier). Headline use: put a third-party S3 bucket under Cairn without trusting it. Opt-in per backend, honest threat model. Design task before implementation: key custody + ranged reads over encrypted objects. |
| Working name "Cairn" | Alpine stone marker. Rename trivial while private; binary is cairnd, port 7420. |
Runner / CI lessons (IMPORTANT — applies to all repos on this Forgejo)
The k3s Forgejo runner executes jobs in HOST mode — directly inside the Alpine act_runner pod. There is no Docker daemon and no job containers. Consequences for every workflow:
container:in a job → fails with "Cannot connect to the Docker daemon"- Node-based actions (
actions/checkout,actions/setup-go, ...) → fail with "Cannot find: node in PATH" - The pod persists between runs → every step must be idempotent (tool installs skip-if-present; checkout must handle a dirty workspace:
git checkout -f+git clean -fdx) - Checkout pattern that works: plain
git init/fetch/checkoutof$GITHUB_SHAusinghttp://x-access-token:${GITHUB_TOKEN}@192.168.10.245/... - Official Go tarballs from go.dev are statically linked → run fine on Alpine/musl; install with runtime arch detection (
uname -m), since jobs can land on amd64 or arm64 - kubectl IS available to job steps but API calls authenticate as the unprivileged
forgejo-runner:defaultServiceAccount → Forbidden. Offline validation (kubeconform) is the right CI tool, not kubectl. GITHUB_TOKENcannot push to the package registry (401 reqPackageAccess). Image-publishing workflows need a dedicatedREGISTRY_TOKENactions secret (write:packageonly). It CAN push code — but workflow-token pushes don't re-trigger workflows (loop prevention).- Job-log API endpoints 404 via gitea-mcp on this Forgejo version — the runner pod's own logs are the reliable source:
kubectl logs -n forgejo-runner deploy/forgejo-runner. rerun_runvia API also 404s — re-trigger tag workflows by deleting and re-creating the tag;dispatch_workflowworks.- The runner doubles as a LAN test host:
interop-rclone.yml(rclone WebDAV),interop-tus.yml(go-tus resumable),interop-ldap.yml(OpenLDAP live-bind),interop-ad.yml(Samba ADsAMAccountName/LDAPS live-bind), andinterop-s3.yml(S3 driver over WebDAV against live Garage) — all manual-dispatch — run against live deployments from inside the network. Each reuses aCAIRN_*actions secret. - CI triggers are
push:[main]+pull_request— pushes to a feature branch do NOT run CI; open a PR to get a run. Use afeat/*branch → PR → squash-merge for reviewable changes instead of committing straight to main. - PR CI runs check out the branch head, not the merge result — a branch that predates a file main's ci.yml needs fails spuriously; update-branch-from-base before trusting the run.
Coordination gotcha: parallel sessions racing the same task (2026-07-20)
Two Claude sessions worked the Keycloak deployment concurrently and both pushed manifests for the same Deployment/Service/PVC — Argo applied both every sync, last-write-wins, until consolidated into one file. Rule: one session per repo/task area at a time, or tell each session the other is active.
Tooling gotcha: Claude Desktop MCP "server hang" that isn't (2026-07-19)
gitea-mcp calls timed out after 4 minutes with a "server unresponsive" symptom. The log showed the server was healthy — the actual line was "Client transport closed (renderer released port)": Claude Desktop's renderer dropped the stdio connection. Fix: restart Claude Desktop. (Distinct from the two real gitea-mcp bugs — sha:"" for new files and the -d stdout corruption.)
Content round-trip gotcha (2026-07-21)
Editing a large existing repo file or wiki page by hand-transcribing its base64 back into a sandbox is unreliable — a single mistyped character (e.g. a look-alike non-ASCII glyph) silently corrupts the decode. Reliable pattern: work from plaintext you actually read (the file's rendered content), apply edits to that, and push the full plaintext via create_or_update_file. Never round-trip 20+ KB of base64 by retyping it. (Corollary for files with ASCII-art diagrams: keep the diagram region untouched where possible; box-drawing glyphs are the easiest thing to corrupt.)
Image gotcha: Bitnami Docker Hub deprecation (2026-07-21)
docker.io/bitnami/* images were emptied in Bitnami's 2025 deprecation — bitnami/openldap:latest now 404s. The images were archived to the bitnamilegacy/ org (same multi-arch, incl arm64, but frozen/unsupported). Fine for throwaway test infra; don't build production on it. Also, for bitnami openldap specifically: the deprecated LDAP_USER_DC + LDAP_GROUP env collide on ou=users and crash tree-seeding with exit 68 (LDAP entryAlreadyExists) — use minimal env (users default to ou=users).
Known limitations (tracked as issues)
- Real-client interop (#2): Finder ✔, rclone ✔, Cyberduck ✔ — all 2026-07-20 against v0.0.2. Windows Explorer untested (nice-to-have).
- OIDC: real-IdP e2e PASSED (Keycloak, 2026-07-20) — browser login + app-password WebDAV both verified; the app-password management UI has shipped.
- LDAP: live-bind fully verified (2026-07-21) — search-then-bind against live OpenLDAP (
uid,interop-ldap.yml) and live Samba Active Directory (sAMAccountNameover LDAPS,interop-ad.yml), each PROPFIND 207 / PUT 201 / GET byte-match / DELETE 204; wrong-password + unknown-user 401. Group resolution (memberOf) not yet exercised (not needed for the bind). - TUS: real-client verified (2026-07-21) —
eventials/go-tusresume-from-offset against the live dogfood (v0.2.0). A real Uppy/tus-js browser upload is still a nice-to-have. - Quota: reporting + WebDAV props shipped (v0.2.1, 2026-07-21) — POSIX statfs; S3 returns 501 (no bucket-wide capacity). WebDAV RFC 4331 props serve on collections (Finder free-space). Quota enforcement is not yet done.
- S3 driver: live-dogfood verified against Garage (2026-07-21,
interop-s3.yml) — the SeaweedFS conformance suite is env-gated and now backed by a live-deployment proof against the actual target (Garage). - Authorization is coarse: any authenticated user sees the whole tree; admins (
auth.admins) can list/revoke any share via/api/v1/admin/*. Per-user home directories and per-file ACLs are designed (ARCHITECTURE.md §5.4) but not implemented. - Go toolchain floor is now 1.25 (go-oidc); CI/release workflows pin go1.22.5 and rely on GOTOOLCHAIN auto-download — bump when convenient.
Test infrastructure (homelab)
- Keycloak (test IdP):
http://192.168.10.248, realmcordyceps, public clientcairn(PKCE S256), redirect URIs for.247/.249. Manifests: homelab-configk3s/keycloak/. Test usernikola-test. - Cairn OIDC test instance:
http://192.168.10.249,v0.0.3,auth.mode: oidc. Manifests:k3s/cairn/test-instance.yaml. - Cairn S3 dogfood (persistent, second dogfood):
http://192.168.10.250,v0.2.1,storage.driver: s3against Garage bucketcairn-dogfood(192.168.10.32:3900, regiongarage),auth.mode: local(usernikola). Config in thecairn-s3-configsecret (endpoint/bucket/region + Garage key + reused nikola hash; not in git). Manifests:k3s/cairn/s3-dogfood.yaml. Re-verify withinterop-s3.yml. - OpenLDAP / Samba AD test directories: both torn down after use (2026-07-21). Manifests were
k3s/openldap/andk3s/samba-ad/(throwaway); the interop workflows (interop-ldap.yml,interop-ad.yml) remain in the Cairn repo for re-runs — recreate the directory + the.250test instance and theCAIRN_*_TEST_PASSWORDsecret to re-verify.
Development notes
make build→bin/cairnd(~5.4 MB static).make releasecross-compiles amd64+arm64.- Releases: push a
v*tag →release.ymlbuilds and pushes the multi-arch image to192.168.10.245/cordy/cairn:<tag>via ko. Latest: v0.2.1 (dogfood.247runs it;cairn-s3at.250runs it too). - Dependency changes: dispatch
go-tidy.ymlon the target ref — regenerates go.mod/go.sum on the runner and pushes back. - Interop retest: dispatch
interop-rclone.yml,interop-tus.yml,interop-ldap.yml,interop-ad.yml, orinterop-s3.ymlfrom the Actions tab — each reuses aCAIRN_*secret. - Feature branches: CI only runs on
push:[main]+pull_request, so usefeat/*→ PR → squash-merge (see Runner/CI lessons). Minor/patch release tags are cut autonomously (owner-approved 2026-07-21). web/static/is the UI source of truth;make webcopies intocmd/cairnd/webdist/forgo:embed.- Module path is
github.com/nikolasomborski/cairn. - Share links are opt-in (
shares.enabled, default off); store files must live on persistent storage, written 0600. - zsh gotcha (interactive):
#does not start a comment by default — pasting commands with trailing# commentspasses them as arguments.
Roadmap snapshot
v0.1 usable daily ✔ complete 2026-07-20 → v0.2 product core (OIDC ✔ + app passwords ✔, real-IdP e2e ✔, app-password UI ✔, LDAP provider ✔ (live-bind verified — OpenLDAP uid + Samba AD sAMAccountName/LDAPS), share links ✔ + UI ✔, TUS ✔ (go-tus resume-verified) — v0.2 complete + fully verified) → v0.3 sovereignty (S3 driver ✔ + live Garage dogfood ✔ 2026-07-21, quota reporting ✔ + WebDAV quota props ✔ (v0.2.1), admin API ✔, quota enforcement pending, age-encrypted storage §4.3 = last remaining v0.3 item) → v0.4 commercial (license files, license server, landing page, GitHub mirror).