Completes review 001 D-1 (vision.md's half landed with ADR-015): - alk-stack.md gitea-lesson item 2: supersession note pointing at ADR-012 §3 / ADR-015 (repo ops are External, scope + manage grant — right mechanism, wrong axis). - AGENTS.md lifecycle status: active OQ list updated (OQ-03 partially resolved, OQ-05 deferred, OQ-16 deferred; OQ-04/06/08 resolved via ADR-013/012/011). - review 001: D-1 marked resolved. All three stale statements were pre-decomposition landmines: research docs are declared 'current source of truth' by AGENTS.md, so the superseded admin-API design needed marking.
125 lines
6.9 KiB
Markdown
125 lines
6.9 KiB
Markdown
# Research: alk stack fit for a git server
|
|
|
|
**Status**: initial pass complete
|
|
**Date**: 2026-09-19
|
|
**Sources**: local sibling crates at `/workspace/@alkdev/{alkcall,alkhttp,alktls,alkvault}`
|
|
(all MIT OR Apache-2.0; alkcall/alkhttp/alktls published on crates.io,
|
|
alkvault published at 0.1.0).
|
|
|
|
## TL;DR
|
|
|
|
The alk stack covers the parts of a self-hosted git server that were
|
|
historically the attack surface: the HTTP and SSH front doors, the wire
|
|
framing, TLS/ACME, and secret storage. alkgit contributes the git brain
|
|
(storage + smart protocol + repo/permission model) and the server binary that
|
|
assembles everything. The architectural match with alkcall is unusually good:
|
|
**alkcall's `BiStream` is exactly the byte-stream abstraction git's
|
|
packetline-based protocol wants underneath it** — the smart protocol is a
|
|
pair of unidirectional streams with request/response framing, which maps
|
|
cleanly onto `BiStream::write_half` / `read_half`.
|
|
|
|
## How the pieces map
|
|
|
|
| alkgit need | alk crate | mechanism |
|
|
|---|---|---|
|
|
| Authenticated transport with per-peer identity | alkcall | `Connection`, `Identity`, `IdentityProvider` |
|
|
| Internal/external operation split | alkcall | `Visibility::Internal` ops are never wire-callable (ADR-017/024) |
|
|
| Per-repo/per-op authorization | alkcall | `AccessControl::check(peer_identity)` + `required_scopes` |
|
|
| HTTP front door for git smart-http + admin API | alkhttp | HTTP/1.1 + HTTP/2 serving, adapters for call protocol |
|
|
| TLS + ACME for the HTTP endpoint | alktls | rustls server configs, cert resolvers, ACME state machine |
|
|
| Encrypted storage of tokens/keys at rest | alkvault | AES-256-GCM vault |
|
|
| Byte streams under pkt-line | alkcall | `BiStream` handed to handlers via `ProtocolHandler::connected` |
|
|
|
|
## The gitea lesson, restated as alkgit design constraints
|
|
|
|
The pwned-gitea incident (CVE-2026-59774) was reachable because an internal
|
|
API was exposed over the internet regardless of auth — the openapi-spec
|
|
pattern of "describe everything, enforce somewhere" is the structural
|
|
antipattern. alkcall already fixes the API-exposure half: only ops the caller
|
|
has privileges for are even visible, and internal ops are structurally
|
|
unreachable from the wire. alkgit must extend the same philosophy:
|
|
|
|
1. **No unauthenticated surface by default.** Even `info/refs` advertisement
|
|
requires an authenticated session unless a repo is explicitly public.
|
|
Anonymous-clone is a per-repo opt-in, not a global default.
|
|
2. **Admin API = internal ops.** Repo/user/permission management rides the
|
|
call protocol as `Visibility::Internal` ops over the admin interface
|
|
(or via alkhttp with auth); there is no admin endpoint that exists on the
|
|
same unauthenticated surface as git traffic.
|
|
*(Superseded by ADR-012 §3 / ADR-015 for alkgit's repo ops: the ops are
|
|
`Visibility::External`, gated by scope + the per-repo `manage` grant —
|
|
the gitea lesson is served by the ACL (visible-surface =
|
|
authorized-surface), not by hiding the ops. "Right mechanism, wrong
|
|
axis.")*
|
|
3. **No plaintext secrets in the DB.** Credentials/tokens go through
|
|
alkvault; the metadata store holds references, not keys.
|
|
4. **Blast-radius thinking carries into the design**: single binary, no
|
|
plugin execution, no markdown renderer by default (a web UI, if it ever
|
|
exists, is a separate phase and separate threat model).
|
|
|
|
## Interfaces in alkcall terms
|
|
|
|
Git transport framing per interface:
|
|
|
|
- **SSH**: the alkcall channels protocol carries a channel whose stream is
|
|
the git pkt-line conversation. The client (`git clone ssh://...`) expects
|
|
an exec of `git-upload-pack '<repo>'`; with alkcall the "SSH channel-like"
|
|
layer provides the framing and identity, and we dispatch on the requested
|
|
command string. alkcall's channel model (channel 0 = call registry, data
|
|
channels = `BiStream`) maps to: one data channel per git session.
|
|
- **HTTP**: alkhttp hosts the smart-http endpoints (`GET
|
|
/<repo>/info/refs?service=git-upload-pack`, `POST /<repo>/git-upload-pack`,
|
|
`POST /<repo>/git-receive-pack`). Smart-http is stateless per request
|
|
(V0/V1) or one-shot (V2), which fits the http adapter model; the response
|
|
body is a pkt-line stream we generate.
|
|
|
|
Both interfaces converge on the same core: **a `BiStream`-like duplex
|
|
session + repo identity + caller identity** → `alkgit-transport` runs the
|
|
smart protocol and produces/consumes packs via `alkgit-core`. The interfaces
|
|
are thin adapters; all policy (auth, ACL, limits) lives before the transport
|
|
layer gets the stream. This is the same "ALPN as a service" shape as
|
|
alktty/alktunnels/alksocks: the git member of the family, one core, many
|
|
front doors, and nothing below the transport boundary knows or cares which
|
|
door produced the stream.
|
|
|
|
## What to verify before architecture commits (POC candidates)
|
|
|
|
1. **BiStream ↔ pkt-line fit**: run a real `git clone` handshake over an
|
|
alkcall `Connection` using `gix-packetline` async codec end-to-end.
|
|
**Verified (POC-1, 2026-09-20)**: real git 2.43 clones/fetches
|
|
fsck-clean over `Connection → BiStream → tokio-util compat →
|
|
gix-packetline`. See `poc-1-findings.md`.
|
|
2. **HTTP smart protocol shape**: confirm alkhttp's streaming response body
|
|
can carry a pack (chunked) and that request bodies stream in without
|
|
buffering the whole POST in memory (receive-pack can be gigabytes).
|
|
**Verified (POC-3, 2026-09-21)**: real git 2.43 clones/fetches
|
|
fsck-clean over `Connection → HttpAdapter → axum custom routes`;
|
|
request bodies stream (`BodyDataStream`, one hyper-read chunk per
|
|
`poll_next` — measured with a dribble probe and git's forced-chunked
|
|
POST path); pack responses stream under back pressure (bounded mpsc →
|
|
`Body::from_stream`, O(counts) RSS on a 60k-object clone). See
|
|
`poc3-findings.md`.
|
|
3. **alkcall backpressure/limits vs git sessions**: large clones are
|
|
long-lived, high-throughput, single-direction streams; confirm channels
|
|
buffer limits (ADR-040) don't fight pack streaming, or route git sessions
|
|
as raw duplex streams rather than chunk-framed channels if needed.
|
|
4. **alktls ACME**: config shape for the http endpoint; likely trivial.
|
|
|
|
## Composability boundary (what "ALPN as a service" implies here)
|
|
|
|
Same family pattern as alktty/alktunnels/alksocks (alknet decomposition):
|
|
one core that is front-door-blind, adapters that each speak one door. For
|
|
alkgit the boundary is concrete:
|
|
|
|
- `alkgit-core` + `alkgit-transport` depend on alkcall types only
|
|
(`BiStream`, identity/ACL types); they never depend on alkhttp or
|
|
alkgit-ssh, and carry no http/channel-specific types below the stream.
|
|
- The http and ssh crates are replaceable: a downstream app (gitea-like)
|
|
embeds the core + transport and brings its own front doors.
|
|
- Admin API = alkcall ops, usable as-is or replaced by a downstream app.
|
|
|
|
## Version pins in workspace manifests
|
|
|
|
- `alkcall 0.8` (published), `alkhttp 0.5` (published), `alktls 0.1`
|
|
(published), `alkvault 0.1` (published). The workspace root already pins
|
|
these; bump via workspace.dependencies when siblings release. |