Changelog · Changelogs
lockhttp changelog
All notable changes to this project are documented here. The format follows Keep a Changelog, and the project uses Semantic Versioning.
Unreleased #
Fixed #
- The quick start with lea (README,
docs/install.md, the site) no longer saysLEA_LOCKHTTP=1 lea, which lea ignores (its configuration wins over the environment): it setsLEA_LOCKHTTP=1in~/.config/lea/lea.conf, thenlea stop --allandlea, and says that lea-proxy and its gate are shared by every lea on the machine (#48).
0.9.1 - 2026-11-13 #
Built on lockkit v0.18.0.
Fixed #
- The launcher mounts the machine's managed directory (
/etc/lockfamily,/Library/Application Support/lockfamilyon macOS) read-only on/etc/lockfamilyin every container, aslox rundoes in its locks:version,gate,signer,caand the other commands now get it too, not onlyconsole,policy,managedand the launcher verbs. A managed path that is a file, a dangling link or holds a:stops the launcher instead of being skipped (#57). - The launcher's managed directory tests pin
uname -s, so they check the Linux and the macOS paths on any runner; they failed on macOS (#59).
0.9.0 - 2026-11-13 #
Built on lockkit v0.18.0. The first full (non pre-release) GitHub release.
Breaking and behaviour changes #
- Breaking:
lockhttp installandlockhttp setupare removed; they are now unknown commands (still exit 3).loxinstalls and updates lockhttp (#40). - Behaviour: the gate checks the deny list (and the managed deny rules) on a CONNECT before a connect grant too, not only before decryption: a host a connect grant used to open is refused when a deny rule matches it (#49).
- Behaviour: with
/etc/lockfamily/managed.tomlpresent, a policy beyond its[lockhttp.bounds]is refused at start and at every poll (exit 3), never asked about (#49). - Behaviour:
vX.Y.Ztags are published as full releases marked Latest, no longer as GitHub pre-releases (#44).
Added #
- The company's managed configuration (lockkit
managed, lockfamilydocs/managed.md): lockhttp applies the[lockhttp]section of/etc/lockfamily/managed.toml.[lockhttp.profiles.<name>]are served as written and cannot be redefined in the policy file;[lockhttp.defaults]fill the keys a profile leaves unset;[lockhttp.bounds](production,mode,heuristics,buffer_limit,connect_grant,idle_timeout,max_session, allowedtrusted,splice,redirect_cdnandinternal_hosts, requireddeny) is a ceiling the console checks at start and at every poll: a policy beyond it is refused (blocked by company policy: …, exit 3, auditedrefused-managed), never asked about.[[lockhttp.bounds.rules]]are deny or ask rules no user rule, grant or shadow mode lifts; they are part of the policy's fingerprint (managed_rules). Without a managed file nothing changes (#49). lockhttp managed showandlockhttp managed check [--profile P](exit 3 and one line per violation;loxruns it before it starts the lock);doctorgains amanagedcheck and checks the project's profiles against the bounds;policy showlists the managed profiles. The launcher mounts the host's/etc/lockfamilyread-only forconsole,policy,managedand the launcher verbs (#49).
Security #
- Configuration and usage errors no longer repeat the value given, which
may be a pasted secret or a URL with userinfo: policy errors name the key
and, for a list, the entry's place in the file (
trusted entry 2,internal_hosts entry 1 host,rules entry 3,inject entry 1; a rule or an injection by its place, not itsidor host) with the allowed values; the sources file names a badnetworksentry by its place;--hub,--instance(gateandhub-key),--hub-net,--require-device,--allow-uid,--listen(gate,--check, signer) anddoctor --gateerrors, flag parse errors, unknown commands, subcommands and flags (an unknown flag is named only when it looks like a flag name), stray arguments and "no profile of that name" omit the value. Profile names are keys and file paths are still named. Themcp_serverswarning (shown and audited) names the server URL without its userinfo, query or fragment. - The gate reloads its secrets file when its content changes even if the rewrite keeps the size and the mtime (a rotation within one mtime tick): it compares the sha256 of the content too (#44).
Changed #
- Releases are no longer GitHub pre-releases: a plain
vX.Y.Ztag is published as a full release marked Latest, and only a tag with a pre-release suffix (vX.Y.Z-rc.N) stays a pre-release, as for the other Locks. CONTRIBUTING.md lists what v1.0.0 waits for (#44). --hubrefuses a URL with a user, password, query or fragment before lockkit (the hub authenticates the instance key), and the gate's start line names the hub host only.- The
mcp_serverswarning keeps the URL's host and path: a secret in the path still shows. - lockkit v0.18.0 (#49).
- The gate checks the deny list (and the managed deny rules) on a CONNECT before a connect grant too, not only before decryption (#49).
Removed #
- Breaking:
lockhttp installandlockhttp setup, which only refused (exit 3, "runs in Docker"): they are now unknown commands (still exit 3).loxinstalls and updates lockhttp (#40).
0.8.1 - 2026-11-07 #
Built on lockkit v0.14.1.
Changed #
- Docs, the agent's hints and the launcher's messages name
lox run, which replaces lockhub's deprecatedlox up(#46):E_NO_CONSOLEsays to start the console withlox run, and so do the gate's usage error,profile addand the console's policy errors. No behavior changes: container labels,up.json,.lockfamily/and the flags stay the same. - lockkit v0.14.1, whose shared texts also name
lox run.
0.8.0 - 2026-11-06 #
Built on lockkit v0.13.0.
Added #
- Proxy-side credential injection (#38, #41). A policy
injectentry has the gate add a header (e.g.Authorization: Bearer …) for one exact host and port, using a secret from its own--secretsfile, so the agent never holds the credential. A request carrying such a secret is refused withE_SECRET_EGRESS, and a missing secret with the new codeE_INJECT.
Changed #
- CI runs the macOS job on
mainand nightly, not on pull requests (#39).
0.7.1 - 2026-11-01 #
Changed #
- Docs and the default policy's comments follow the first run:
lox add lockhttpsets a lock up; no auto mode (anautorule asks likeask, every hub ticket is NeverAuto); the commented sample rule asks.
Added #
- Each release tag bumps the
lockhttpformula of the Homebrew tap (brew install davidgodefroid/tap/lockhttp): the release workflow's last job dispatches the bump once the release is signed (DavidGodefroid/lockfamily#8). It is skipped while theTAP_DISPATCH_TOKENsecret is not set.
0.7.0 - 2026-11-01 #
Built on lockkit v0.10.0; supported lockhub as v0.6.0. The launcher verbs
of lockkit v0.10.0 (lockkit#111, GUIDE.md §14), which lox init runs on
every lock (#31, #32). Breaking: lockhttp init --trusted is removed (now
profile add --trusted).
Added #
lockhttp profile add [--non-interactive] [--json] [--project DIR] [--profile NAME] [--production] [--trusted "a.com .corp.example"]: adds a profile with the default policy to the project's.lockhttp/config.toml, whichlox upreviews and copies into the console. A profile already there is never rewritten (codeexists, nothing written); other profiles are kept byte for byte. Parsed with lockkit'sverbs.Parse: a flag named like a secret, a value that embeds a password or a positional argument is refused, and no error quotes a value. lockhttp's target needs no secret: no--credentials, nocredentialsin the JSON. Without--non-interactiveit asks on the terminal for the name, production and trusted hosts, and refuses without one.--jsonprints oneverbs.Profileorverbs.Errorline.lockhttp init <agent>(claude,codex,cursor,gemini): writes nothing (lockhttp has no agent files: lea routes the agent's HTTP through the gate), says so and exits 0.lockhttp doctor --fix [--yes]repairs the write permission others have on the project's policy or its directory, asking before each repair unless--yes(required without a terminal;--yesalone is a usage error).--jsonprints oneverbs.Doctor(orverbs.Error) line.
Changed #
lockhttp doctorchecks the project of the current directory (its.lockhttp/config.toml, every profile unless--profile) instead of the console account's policy, and--configis gone.- The launcher runs
profile,initanddoctoras you, with the project mounted at its own path and the current directory as the working directory (profile add --projectmounted and made absolute), without the volumelockhttp-home;policyandconsolekeep the console account. - lockkit v0.10.0.
Removed #
lockhttp init [--profile] [--config] [--trusted] [--force], which wrote the console account's policy (pre-release, no alias). Its--trustedis nowprofile add --trusted, named after the config key it fills;--profileisprofile add --profile.
0.6.0 - 2026-11-01 #
Hub-only release, built on lockkit v0.9.1. The gate runs hub-attached
only (lockkit#110): one mode, lox up's.
Removed #
- The standalone gate.
lockhttp gatewithout--hub,--hub-key-fileand--instanceexits with a usage error ("launched bylox uponly"); lea passes them fromlox up'sup.json. The gate never asks a console about a held request any more: every ask is a hub ticket. - The rollback of hub mode (a gate started without its hub flags
withdrawing its pending hub tickets) and its refusal code
E_WITHDRAWN(#26). lockhttp console --auto, auto mode, and the console'shttp.approvemethod: the console serves the approved policy (policy.get) and asks its human about loosenings only. Averdict = "auto"rule asks likeask(hub tickets areNeverAuto).
Changed #
lockhttp consoleis lockkit's policy-only console (console.Options.PolicyOnly, lockkit v0.9.1): it takes no hub flags, aslox upstarts it, opens no hub client and approves no request. Only the gate is hub-attached.- The hub's decision on a held request is audited once, by the gate,
with
"approval": "hub", the ticket, the approver and the hub's signature. The record of a request run after the hub approved it within the hold says"approval": "hub-run"(was"console"), so that lockhub'slox audit verify --lockdoes not read it as a second, unsigned hub decision. - The agent's hints name
lox upforE_NO_CONSOLEand lockhub for an approval. - Docs: the quick start goes through
lox addandlox up; the standalone quick start is gone. The launcher's docker e2e checks that the gate refuses to start without the hub; requests through a hub-attached gate are covered by the integration tests (lockkit's test hub) and lockhub's e2e. - Flag help (
--devices-file,--facade-keys-file), comments and docs namelox, the host CLI's name since lockhub v0.3.0 (#29).
0.5.1 - 2026-10-31 #
lockkit v0.8.1: a hub URL is http(s)://host[:port] only, lockkit's errors
no longer repeat the value given, and previews mask after JSON escapes.
Security #
- lockkit v0.8.1: lockkit's errors no longer repeat the value given (the
hub URL, transport errors, instance names,
--require-devicerisk classes,hub-key), and--hubtakeshttp(s)://host[:port]only: a user, password, path, query, fragment, IPv6 zone, non-DNS host or bad port is refused (lockkit's hub client). lockhttp's own messages that still quote a value are addressed separately.
Changed #
- Builds on lockkit v0.8.1. The body preview and the personal-data
heuristic read a
\n,\r,\tor\u0026-style escape as the blank or delimiter it stands for (lockkit v0.8.0), so a phone number, IBAN or card number right after one in a JSON body is masked too. lockhttp console(lockkit v0.7.0, v0.7.1): a policy tightening that cannot be saved is applied anyway (audited, told on the console) instead of leaving the looser policy in force, and a change that ends the session no longer prints a "policy tightened" or "policy change not applied" line after it.
0.5.0 - 2026-10-30 #
Hub mode on lockkit v0.6.0: expired tickets are approved again, reveal from a local facade, and a gate started without the hub withdraws its hub tickets.
Builds on lockkit v0.6.0 (reveal and re-approval; a renewal that ran or was denied is not renewable, an expired renewal is renewed in turn).
Added #
- Hub mode: an
E_PENDINGnames the hub ticket the request waits on (X-LockHTTP-Ticket). - Re-approval of expired tickets (lockkit H1): a ticket that expires
undecided is renewed, as a new ticket naming the expired one, while the
agent still sends the request, or when it sends it again; the agent gets
E_PENDINGwith the new ticket, and an approval lets the request through once; in hub modeE_TIMEOUTnames the expired ticket and its hint says to send the same request again. Renewals re-check the plan as a run does (SetRecompute; plan inputsrequestandpolicy). - Reveal (Ctrl-O on a local facade of the hub):
--facade-keys-file(LOCKHTTP_FACADE_KEYS_FILE) pins the facade keys; each ticket keeps the console's clear screen in the gate's volume, sealed to a pinned facade only. Reveals and renewals asked by the hub are audited (reveal,renew). - Rollback of hub mode: a gate started without the hub flags withdraws the
tickets its hub mode left pending (the next start in hub mode tells the
hub) and answers their request once with the new code
E_WITHDRAWN, without asking the console or sending it.
Changed #
- Hub mode: the
E_TIMEOUTreason says the ticket "expired before it was used" (it also covers an approval that aged before use); the security model (T18) says a clear screen also leaves sealed to the pinned devices with--seal-clear(#19).
0.4.0 - 2026-10-30 #
Hub mode: decisions taken on lockhub's mobile facade, signed by a pinned phone.
Added #
- Hub mode: the gate pins approval devices, with the same flags as lockkit's
console:
--devices-file(LOCKHTTP_DEVICES_FILE),--require-device(LOCKHTTP_REQUIRE_DEVICE) and--seal-clear(LOCKHTTP_SEAL_CLEAR=1). A decision of the hub's mobile facade runs only when signed by a pinned device; without one it is refused, as it always was before. With--seal-clearthe clear screen goes to the devices sealed, the hub relaying ciphertext. Uses lockkit's device decisions (hubclient.Config.Devices).
0.3.0 - 2026-10-30 #
Hub mode: the gate can hand its asks to lockhub instead of its own console.
Changed #
- Hub mode: an
http://--hubURL is refused unless the host is a loopback address, or--hub-plain-http(LOCKHTTP_HUB_PLAIN_HTTP=1) is given for a hub on a network of its own (a service name without a dot or a private address); redirects from the hub are refused and the hub state file must be private (lockkit#57).
Added #
- Hub mode for the gate (
--hub,--hub-key-file,--instance,--home,--hub-net): asks become signed tickets on lockhub, and a verified decision grants the existing single-use authorisation or connect grant. The gate refuses the hub's addresses as targets, even withinternal_hosts.lockhttp hub-keyprints the instance key to pin. Builds on lockkit v0.3.1 (hub support).
0.2.1 - 2026-10-28 #
Fixed #
lockhttp ca initunder rootless Podman: the launcher adds--userns keep-id, so the CA files are written as the invoking user (they failed with "permission denied" on a subuid).- The image's
lockhttp versionprints0.2.1, notv0.2.1, like the release archives and the launcher.
0.2.0 - 2026-10-28 #
Changed (breaking) #
- LockHTTP runs in Docker only. On the host,
lockhttpis now a launcher, a POSIX shell script (scripts/lockhttp) that runs every command in the release image, each as the account it needs:consoleaslockhttp(uid 10021) in the containerlockhttp-console, with no log driver and the volumeslockhttp-runandlockhttp-home;init,policyanddoctoraslockhttp;gateasgate(uid 10003, inlockhttp-clients) withlockhttp-run, published on127.0.0.1:3130;signerassigner(uid 10004);caas the invoking user. Seedocs/docker.md. scripts/install.shinstalls the launcher, not a binary: it verifies the cosign signature ofchecksums.txtand of the image, and writes the image's verified digest into the launcher, which runs it by digest (LOCKHTTP_SKIP_COSIGN=1: by tag, unverified). It warns when a former host install (/etc/lockhttp/system.toml) remains.- lea integration (
docs/lea.md): the gate and the signer run as sidecar containers from the image (--network container:<prefix>-proxy,--user gate, volumelockhttp-run). Noallowed_uids, subuid mapping,--group-add, host/run/lockhttporLOCKHTTP_RUNTIME_DIR. The file formats are unchanged (contract version 1). lockhttp doctorno longer checks that the binary is root-owned, and reportsallowed_uidsonly when set.
Added #
- The image
ghcr.io/davidgodefroid/lockhttp:X.Y.Z(amd64, arm64), built by the release workflow and signed with cosign; aDockerfilewith the accountslockhttp,gateandsignerand the build argumentALLOWED_UIDS. lockhttp policy show|edit|path [--config FILE]: print, edit (with a parse check) or locate the console account's policy.lockhttp update [vX.Y.Z]andlockhttp versionin the launcher: a release's launcher, checked asinstall.shdoes, thendocker pull; the launcher's and the image's versions.LOCKHTTP_IMAGE,LOCKHTTP_DOCKER_ARGSandLOCKHTTP_PUBLISHfor the launcher.- CI: the image built and driven through the launcher end to end
(
test/docker/e2e.py).
Removed #
lockhttp install(andsetup): the image refuses them. The host accountlockhttp, the grouplockhttp-clients,/etc/lockhttp/system.toml,/run/lockhttpand the host binary are no longer created; see "Migrating from a host install" indocs/docker.md.- The
.deb,.rpmand.apkpackages. Release archives now carry the launcher.
0.1.0 - 2026-10-28 #
First pre-release. Phase 0 and 1 of the design (CONNECT + SNI gate in front
of squid, interactive traced allowlist), with phase 2 decryption (local CA,
separate signer, Claude API allowlist) available behind --ca-cert and
--signer. Built on lockkit v0.2.1.
Added #
lockhttp gate: the HTTP(S) proxy. CONNECTs are decided at the domain (an approval opens reads AND writes, and the screen says so), or decrypted per request when a CA and a signer are given. Plain HTTP is decided per request. Verdicts FREE, ASK, DENY; refusals answer403withX-LockHTTP-CodeandX-LockHTTP-Request.- Hard floor no policy loosens: IP literals, DoH, cloud metadata names,
ports other than 80/443 (except an internal host's declared ports),
single-label hosts, SNI different from the CONNECT host, an incomplete or
missing ClientHello on 443,
Upgrade: h2c, nested CONNECT, a Host header that differs from the target. Direct mode never dials a non-public address. - Policy (
~/.config/lockhttp/config.tomlof the console account): trusted read hosts, splice and deny lists, internal hosts, redirect CDNs, rules (free,auto,ask,deny) scoped to a container or to all, never to an agent;mode = "shadow"andheuristics = "shadow" | "enforce" | "off".autois refused on messaging, registry publishing,git-receive-pack, spliced hosts, every-host rules and production profiles. - Claude API allowlist (decrypted):
/v1/messagesfree only withoutmcp_servers,containeror a server tool other thanweb_search; ambiguous JSON asks;/v1/filesasks; a credential that is not the container's is denied; other Anthropic hosts and paths ask (NeverAuto). - Secret egress denial (GitHub, Anthropic, AWS, Slack, OpenAI, Google, npm, GitLab tokens and private keys outside their hosts) and an exfiltration score (URL size, entropy, subdomains, headers, personal data), logged in shadow by default.
lockhttp console: lockkit's console on the host, in its own account (http.approve,policy.get). Every screen isNeverAutounless an approvedautorule matches, re-checked by the console itself. Masked body preview, git push refs, pending count, "client gone" note.- Ask queue: one approval at a time; identical fingerprints (container,
method, URL, significant headers, normalised body) share a decision, other
bodies are separate items;
E_PENDINGafter the hold time, then a single-use authorisation (10 min) once approved. - Fail-closed: no console → asks refused at once (
E_NO_CONSOLE), trusted reads go on with the last approved policy; unknown source →E_SOURCE_UNKNOWN. - lea integration (
docs/lea.md, contract v1):sources.json(container by local address, agent by Proxy-Authorization, keyproxy viaX-Lea-Source),credentials.json, status file, gate JSONL audit (daily, 30 days). lockhttp signer(holds the CA key, issues 24 h leaves to allowed uids) andlockhttp ca init(P-256, pathlen 0, 90 days).lockhttp install(console account, clients group,allowed_uidsfor the gate, rootless subuid mapping),lockhttp init,lockhttp doctor.- Release pipeline: goreleaser, cosign keyless signatures, SBOMs,
SHA-pinned actions,
scripts/install.shwith signature verification.