id: BB-RESEARCH-RMUENXZX type: research title: Luigit Soft Serve public metadata and SSH signing identity
Luigit Soft Serve public metadata and SSH signing identity
Scope
Determine how Luigit can receive authoritative public repository metadata without inheriting Soft Serve secrets or unstable internals. Determine whether Soft Serve authentication keys can bind Git SSH signatures to identities.
Research targets Soft Serve v0.12.2.
The deployed version is unknown because current infrastructure tracks charmcli/soft-serve:latest with registry auto-update.
Facts
Soft Serve has no complete metadata API
Soft Serve HTTP exposes health, Git transport, Git LFS, and Go import discovery routes. It exposes no repository catalog, owner, visibility, user, or public-key API.
The SSH command interface exposes repository metadata as human-oriented text. Administrative commands can list users and their keys. The output is not a versioned machine contract, requires SSH authorization, and repository inspection may depend on Git state.
Webhooks are event streams rather than complete snapshots. They omit hidden state and the complete user-key mapping, and cannot reconcile missed events without another source.
Soft Serve's importable Go packages expose the required data but are internal application packages in a pre-1.0 module. Importability is not an API stability promise and does not reproduce every transport authorization decision. A Rust Luigit cannot use them in-process without adding an unsuitable language boundary.
Sources: Soft Serve v0.12.2 HTTP router, Git routes, repository commands, repository list, backend repository model, SemVer 2.0.
SQLite contains the required metadata and unrelated secrets
The default SQLite database stores repository name, project name, description, private state, hidden state, owner user ID, users, and SSH public keys. A repository-to-user join supplies Luigit's name, owner, description, and visibility fields.
The same file also stores password hashes, access-token hashes, webhook secrets, webhook request and response headers, and webhook bodies. SQLite read-only mode prevents writes but cannot restrict tables, rows, or columns. Mounting this database into a public long-running Luigit process would violate least privilege even if application queries select only public columns.
SQLite immutable=1 is invalid for the live Soft Serve database because it disables locking and change detection while another process may modify the file.
A trusted reader may use a normal read-only connection and one read transaction for a consistent snapshot.
Sources: SQLite schema, webhook schema, SQLite omitted authorization, SQLite URI options, SQLite isolation, SQLite WAL.
Existing deployment already uses an isolated database reader
The Stagit renderer reads soft-serve.db only while running as a networkless one-shot container.
It copies owner and description into bare-repository side files, then publishes repositories carrying git-daemon-export-ok.
This boundary is materially safer than mounting the database into a public server because the reader is short-lived and has no network. It is not sufficiently fresh: the timer runs every five minutes, and previously rendered private content remains public until a later successful render.
Sources: modules/klops/stagit/render-stagit.sh, modules/klops/quadlets/stagit-render.container, modules/klops/quadlets/stagit-render.timer, modules/klops/templates/Caddyfile.tftpl.
Soft Serve visibility has three meanings
Repository public state is private = false.
Soft Serve creates git-daemon-export-ok for this state and removes it for private repositories.
The database update and filesystem marker update are not one atomic transaction, so disagreement is possible.
hidden controls discovery, not authorization.
The default SSH browser omits hidden repositories, while direct Git access and an explicit all-repositories listing can still reach readable hidden repositories.
Effective anonymous transport access also depends on anon-access, allow-keyless, credentials, and transport type.
These settings govern Soft Serve transports; they do not govern Luigit's separately public HTTP boundary.
Current Stagit publication follows the marker only. It ignores hidden state and global anonymous-access settings.
Sources: Soft Serve access calculation, settings, private-state changes, SSH browser listing, authentication documentation.
A public manifest is the narrow declassification boundary
A generated manifest can contain only data approved for Luigit:
- Format version and Soft Serve source version.
- Source schema fingerprint or migration version.
- Monotonic generation, creation time, and expiry time.
- Public repository name, owner username, description, and hidden state.
Private repository names, user directories, credentials, tokens, webhook data, and database internals need not cross the boundary. The exchange directory can be writable only by the trusted producer and mounted read-only into Luigit. A signature adds no value while producer, exchange, and consumer remain on one trusted host with enforced filesystem ownership.
The safe data flow is Soft Serve database plus repository markers → isolated trusted exporter → atomic public manifest → Luigit.
Manifest freshness is an authorization property
The exporter must validate an explicitly supported Soft Serve schema, read one consistent transaction, and require agreement between private = false and git-daemon-export-ok.
Unknown schema, malformed rows, duplicate repository names, missing owners, missing repositories, or marker disagreement must abort the whole generation.
The producer must write a temporary file, flush it, atomically rename it, and flush the exchange directory. A failed generation must not replace the previous manifest or extend its expiry.
Luigit must deny repositories absent from the manifest. It must reject missing, malformed, unknown-version, expired, future-dated, or generation-regressed manifests. After expiry it must fail readiness and deny repository routes rather than continue serving a stale allowlist.
Checking the current marker on each request closes normal public-to-private transitions immediately because Soft Serve removes the marker before committing private state. Manifest membership keeps private-to-public transitions denied until trusted regeneration.
Sources: Soft Serve private-state ordering, POSIX rename.
Soft Serve key storage is current authentication registry state
The public_keys table maps one stored authorized-key string to one user row through a unique constraint.
Normal writes parse an authorized-key line and store the canonical key type plus base64 wire value without comments or options.
Usernames are lowercased but mutable, deletable, and reusable.
Keys can be removed and later assigned to another user.
The schema has no key purpose, signing authorization, proof-of-possession status, validity interval, revocation status, or immutable binding history. Deletion removes the current association rather than recording its end.
Legacy migration copied old key strings verbatim. A reader cannot trust textual uniqueness alone; it must parse key rows and reject semantic duplicates or malformed keys.
Sources: user and key schema, key store, SSH key utilities, migration.
Adding an authentication key proves no possession of that key
A user authenticated with one existing key may add another arbitrary public key to the account. Soft Serve parses and stores the new public key without requiring a signature or login by its private counterpart. An administrator may likewise assign keys to users.
An account holder could therefore copy an unregistered public key from a signed Git object, register it, and create a false current association with signatures made by somebody else. The database mapping is an administrative assertion, not cryptographic proof that the account controls the registered private key.
A successful later login with that key would prove possession at that moment, but Soft Serve stores no durable successful-use event or verified-at state.
Source: Soft Serve self-service key addition.
SSH signature validity still provides useful independent evidence
A Git SSH signature covers exact commit or annotated-tag bytes.
The SSHSIG envelope contains its public key, namespace, hash algorithm, and signature, but no username, email, account ID, or trusted signing time.
Git uses namespace git.
Cryptographic verification proves that the signing capability produced a signature over the exact object payload. It does not prove the truth of author, committer, tagger, email, or timestamp fields inside that payload.
RSA public keys retain the ssh-rsa key-format label even when the signature correctly uses RSA-SHA2.
Key matching must compare parsed key type and full serialized key bytes, not comments, filenames, author email, or display strings.
A SHA-256 fingerprint is suitable for display but should not replace full-key comparison in the decision.
Sources: Git signature format, OpenSSH SSHSIG protocol, RFC 8332.
SSH trust and revocation require separate policy
Git's allowed-signers model maps principals to keys outside the signature. It can restrict namespaces and validity intervals and can designate certificate authorities. Git also supports a separate SSH revocation file or key-revocation list.
Soft Serve registration provides none of these signing-policy dimensions. Removing an authentication key does not establish that it was compromised or historically revoked. Keeping it registered does not establish that another authority has not revoked it.
Raw-key registration and SSH certificate authority are different trust modes. Soft Serve discards authorized-key options, so a stored key must never be inferred to be a certificate authority. A certificate must not be unwrapped and matched to an account's raw subject key.
Sources: Git SSH signing configuration, OpenBSD ssh-keygen.
Version drift is security-relevant
Soft Serve v0.12.2 is a security release. Earlier 0.11 releases included a critical SSH authentication bypass in key-to-user session handling. The project currently publishes multiple security advisories.
A floating image can change database shape, access semantics, and key handling without a corresponding Luigit change. Direct schema integration needs explicit compatibility tests against the deployed schema. The service update policy keeps automatic updates enabled rather than freezing the image to avoid compatibility risk.
Sources: Soft Serve v0.12.2 release, authentication bypass advisory, Soft Serve advisories.
Conclusions
Use a versioned public manifest as Luigit's only Soft Serve metadata input.
Do not mount soft-serve.db, Soft Serve configuration, access credentials, or SSH host keys into the public Luigit container.
The manifest producer belongs in a trusted, networkless boundary with read-only database and repository access and write access only to the exchange directory. The boundary may reuse the current networkless one-shot pattern, but must refresh within seconds and publish atomically. Whether it is an upstream Soft Serve export, a repository-managed adapter, or another mode of the Luigit executable remains an implementation decision constrained by the one-container law.
Define Luigit repository publication as the intersection of:
- A current unexpired manifest entry derived from
private = false. - A current
git-daemon-export-okmarker. - A present repository at the canonical declared path.
Treat hidden as discovery metadata, not privacy.
Until product policy says otherwise, omit hidden repositories from catalog and search while permitting direct access through the same public authorization checks.
Never use anon-access or allow-keyless as implicit Luigit authorization because they describe different Soft Serve transports.
Soft Serve authentication-key registration is not an identity verification authority.
It lacks proof of possession for added keys, signing purpose, history, revocation, and stable human identity.
Luigit may not convert a matching row into verified identity, verified author, signed by user, or equivalent language.
If exposed at all, the only sound statement is: This signing key is currently registered as an authentication key for Soft Serve user <name>.
That registry fact must remain visually subordinate to independent signature validity and carry no trust status.
The minimal v1 should omit this enrichment because it cannot satisfy the verified-identity requirement.
To make Soft Serve authoritative later, the binding mechanism must add proof of private-key possession, explicit signing purpose, stable account identity, binding start and end records, revocation semantics, actor provenance, and an append-only history. A dedicated operator-controlled allowed-signers manifest may be smaller than extending Soft Serve's account model.
Unresolved questions
- Does the one-executable and one-container law permit a separate trusted metadata exporter outside the public Luigit runtime?
- Should the producer be an upstream Soft Serve export, a small repository-owned adapter, or a separate mode of the Luigit executable?
- What maximum public-to-private revocation delay is acceptable, and are all visibility changes guaranteed to pass through Soft Serve?
- What exact Soft Serve image digest, migration version, journal mode, and schema are deployed at cutover?
- Should hidden public repositories remain directly addressable, or should Luigit deny them entirely?
- Do bare repository configuration, remotes, alternates, or hooks contain sensitive data that a public read-only mount could disclose after compromise?
- Is current Soft Serve registration enrichment useful enough to justify publishing usernames and authentication-key material into the metadata boundary?
- What authority defines SSH signing principals, proof of possession, revocation, certificate authorities, and key-validity intervals?
- Is verified identity limited to a local stable account, or must it name a human with an independently verified external identity?
- Should historical identity survive key removal and username changes, and if so, which append-only authority records those transitions?