Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
36 commits
Select commit Hold shift + click to select a range
03953d9
feat(node): add a persistent libp2p identity key file
beardthelion Aug 11, 2026
ff1f8ea
fix(node): load the libp2p identity from the persistent key file
beardthelion Aug 11, 2026
8951f53
fix(node): create the p2p key with 0600 at creation and reject a loos…
beardthelion Aug 11, 2026
e5c88d9
fix(node): publish the p2p key atomically and pin its directory
beardthelion Aug 12, 2026
b516868
style(node): comma instead of a dash in the key-load warning
beardthelion Aug 12, 2026
4259c91
fix(node): require the p2p key path to name a directory
beardthelion Aug 14, 2026
900bf53
test(node): run the key-permission probe in its own process
beardthelion Aug 14, 2026
ee01ec1
docs(node): scope the p2p key permission claim and note the rotation
beardthelion Aug 14, 2026
6cd0cb2
fix(review): close a `..` escape from the p2p key-path check
beardthelion Aug 14, 2026
9e8c97b
fix(review): reject `..` in an absolute p2p key path too
beardthelion Aug 14, 2026
40eac8a
fix(node): scrub the serialized p2p private key from memory
beardthelion Aug 14, 2026
531f49c
fix(node): validate the p2p key path before securing its parent
beardthelion Aug 28, 2026
3695a0a
fix(node): satisfy clippy single-element-loop on p2p path test
beardthelion Aug 28, 2026
f4734ed
fix(node): tighten the p2p key storage contract before mutation
beardthelion Aug 28, 2026
fc0e52c
fix(node): refuse foreign-owned p2p key paths and cap reads on all pl…
beardthelion Aug 28, 2026
b6cf5d4
fix(node): hold the p2p key directory open across its trust checks
kevincodex1 Aug 31, 2026
b03acba
fix(node): walk the p2p key ancestor chain descriptor-anchored
beardthelion Sep 1, 2026
b30a2fd
fix(node): refuse group-writable and non-directory p2p key ancestors
beardthelion Sep 1, 2026
009cb99
fix(node): verify the cwd anchor for relative p2p key paths
beardthelion Sep 1, 2026
0cc1a1c
test(node): stop rendering uids in p2p assert-message strings
beardthelion Sep 1, 2026
6d2b3cb
test(node): add walk rows to the p2p key-storage contract matrix
beardthelion Sep 1, 2026
31561a9
docs(node): note the verified working directory for relative p2p key …
beardthelion Sep 1, 2026
5352866
fix(node): pin and verify the mode of every key object this process c…
beardthelion Sep 2, 2026
2d5f6e9
fix(node): create and publish the node identity key through the pinne…
beardthelion Sep 2, 2026
5fcd5d4
fix(node): split p2p key config validation from live storage validation
beardthelion Sep 2, 2026
570b6f1
docs(node): state the two-domain key-path policy and how to see a deg…
beardthelion Sep 2, 2026
ca31f33
test(node): pin the degrade signal and wait for both startup lines
beardthelion Sep 2, 2026
ad43fc5
fix(node): keep key publication transactional and path-policy split
beardthelion Sep 3, 2026
8db66fe
fix(node): refuse a writable cwd for a bare identity key
beardthelion Sep 3, 2026
0a63844
fix(node): load identity without following pathnames
beardthelion Sep 4, 2026
6faf675
fix(node): parse key paths, leave identity parents, repair special modes
beardthelion Sep 4, 2026
2c3aaca
fix(node): type-check the non-unix p2p scratch writer
beardthelion Sep 4, 2026
c27a72c
fix(node): refuse identity path spellings that Path would retarget
beardthelion Sep 4, 2026
2d9cfff
fix(node): create every missing identity parent on one pinned walk
beardthelion Sep 6, 2026
38a289b
test(node): race real concurrent first boots of the identity key
beardthelion Sep 6, 2026
4d4063a
fix(node): resolve identity key path components one at a time
beardthelion Sep 6, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
31 changes: 30 additions & 1 deletion .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,39 @@
# All variables are optional unless marked REQUIRED.

# ── Node identity ─────────────────────────────────────────────────────────
# Path to the node's Ed25519 keypair PEM file.
# Path to the node's Ed25519 keypair PEM file. Must name a key file, not a
# directory: a trailing slash, a final `.` or `..`, and `..` traversal are
# refused so the path cannot retarget a parent. A `~/` value must stay under
# home after expansion. Bare `identity.pem` is still legal.
# Generate with: gl identity new
GITLAWB_KEY=/data/keys/identity.pem

# Path to the node's persistent libp2p identity key file. Must include a
# directory, because the node will not keep its p2p identity key in the working
# directory. A value that cannot name a securable key file (a bare filename,
# `..` traversal, a trailing slash, the filesystem root, or a `~/` path that
# escapes home) is refused before the node binds, so the process exits.
# Anything wrong with the storage itself (a symlinked or non-directory parent,
# an unsafe cwd or ancestor, an unreadable or malformed key) leaves p2p off
# while HTTP keeps serving, logged at error as p2p_identity_key_load_failed and
# counted by the gitlawb_p2p_identity_key_load_failed metric. /health still
# reports healthy in that state, so alert on the event or the metric. Fixing
# the storage and restarting restores the same PeerId.
# On Unix it is created 0600 inside a 0700 directory. A key directory that
# grants access beyond its owner is tightened to 0700 on start; one closed too
# far is refused with a chmod 700 remedy rather than widened. On other
# platforms no permissions are enforced.
# If the node logs that it tightened a key directory that was GROUP OR WORLD
# WRITABLE, treat the key that was sitting there as possibly exposed: delete
# it so a fresh identity is generated on the next start. An ordinary 0755
# directory is tightened too, but others could not read a 0600 key or replace
# its directory entry, so do not rotate for that case. A directory that was
# merely closed too far is refused, not tightened, and no key in it was ever
# exposed.
# Keep it on a persistent volume so the PeerId survives redeploys.
# Default: ~/.gitlawb/p2p.key
#GITLAWB_P2P_KEY=/data/keys/p2p.key

# Publicly reachable URL of this node (used in peer announcements)
GITLAWB_PUBLIC_URL=https://your-node.example.com

Expand Down
1 change: 1 addition & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,7 @@ WORKDIR /data

ENV GITLAWB_REPOS_DIR=/data/repos \
GITLAWB_KEY=/data/keys/identity.pem \
GITLAWB_P2P_KEY=/data/keys/p2p.key \
GITLAWB_HOST=0.0.0.0 \
GITLAWB_PORT=7545 \
GITLAWB_P2P_PORT=7546
Expand Down
23 changes: 23 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -397,6 +397,7 @@ Important node settings:
| `GITLAWB_P2P_PORT` | libp2p QUIC/UDP port. Use `0` to disable. |
| `GITLAWB_BOOTSTRAP_PEERS` | Comma-separated HTTP peer URLs. |
| `GITLAWB_P2P_BOOTSTRAP` | Comma-separated libp2p multiaddrs. |
| `GITLAWB_P2P_KEY` | Path to the node's persistent libp2p identity key file, which fixes the PeerId across restarts. Must name a key file inside a directory, such as `/data/keys/p2p.key` or `./keys/p2p.key`. Two kinds of problem are handled differently. A value that cannot name a securable key file at all (a bare filename, `..` traversal, a trailing `/`, the filesystem root, or a `~/` path that escapes home) is refused before the node binds, so the process exits. Anything wrong with the storage itself (a symlinked or non-directory parent, an unsafe ancestor or working directory, an unreadable or malformed key) leaves p2p off while HTTP keeps serving; the node logs it at error level as `p2p_identity_key_load_failed` and sets the `gitlawb_p2p_identity_key_load_failed` metric to 1. Note that `/health` still reports healthy in that state, so alert on the log event or the metric rather than on the health check. Correcting the storage and restarting restores the same PeerId. On Unix a new key is created `0600` inside a `0700` directory. A key directory that grants access beyond its owner is tightened to `0700` on start; one that is closed too far is refused with a `chmod 700` remedy rather than widened, since a directory locked down on purpose is an operator decision. Rotate the key only when the node logs that the directory was group- or world-writable (or the key file itself was group- or world-readable). An ordinary `0755` directory is tightened too, but others could not read a `0600` key or replace its directory entry. An existing key file is never modified: one with group or other bits set is rejected (`chmod 600`), and so is one this node cannot read. On other platforms no permissions are enforced. Default `~/.gitlawb/p2p.key`; point it at a persistent volume when running in a container. |
| `GITLAWB_BOOTSTRAP_DISABLE_SEEDS` | Disable embedded seed peers for isolated dev/test networks. |
| `GITLAWB_REQUIRE_SIGNED_PEER_WRITES` | Require peer writes to prove their DID on every transport: RFC 9421 signatures on the announce/sync routes, and an Ed25519 payload signature on gossip ref-update events. Defaults to `false` during the staged rollout below. Upgrade every gossip publisher before enabling, or their updates are dropped with no error on their side. |
| `GITLAWB_ENFORCE_OWNER_PUSH` | Require the authenticated pusher to be the repo owner on `git-receive-pack`. **Defaults to `true`.** A `did:key` signature is authentication, not authorization — anyone can mint a key and sign — so with this off every signed caller may push to every repository, private ones included. Delegated and CI keys count as non-owners: a UCAN `git/push` capability is verified but not yet honored for authorization, so they cannot push while this is on. Set `false` only for a rolling upgrade; see [`docs/RUN-A-NODE.md`](docs/RUN-A-NODE.md). |
Expand Down Expand Up @@ -426,6 +427,28 @@ Production note: change the default Postgres password before exposing a node pub

Legacy-pin window: releases before the CID-resolver work stored the provider CID (Kubo dag-pb / Pinata) as a pinned object's resolver key. The `/ipfs/{cid}` resolver now recomputes the raw-content CID from the object bytes and refuses to serve a key that does not match, so `GET /api/v1/ipfs/pins` can still advertise an unrepaired legacy CID that 404s. Such a row is repaired opportunistically the next time a push carries the object again (its key is rewritten to the raw CID, the old value kept in `legacy_provider_cid`), but git negotiation omits objects the node already has, so most legacy rows never re-enter a push delta. A deferred one-shot startup sweep, not this opportunistic path, is what fully retires the advertise-then-404 window. Rows whose object bytes are gone stay withheld.

### Upgrading: the PeerId rotates once

This node's libp2p identity is now a keypair generated on first start and kept
at `GITLAWB_P2P_KEY`, rather than one derived from the node DID. Every node
therefore gets a new PeerId once, on the first start after upgrading, and keeps
it from then on as long as that key file survives (put it on a persistent volume
in a container).

Two things to check before upgrading:

- Any `GITLAWB_P2P_BOOTSTRAP` multiaddr that pins a peer's old PeerId with a
`/p2p/<PeerId>` suffix stops matching once that peer upgrades. Update the
suffix, or drop it and let identify supply the current one. Addresses without
the suffix keep working untouched.
- `GITLAWB_P2P_KEY` must name a key file inside a directory, such as
`/data/keys/p2p.key` or `./keys/p2p.key`. A bare filename is refused at
startup, since the node will not keep its identity key in the working
directory.

Peers found over `GITLAWB_BOOTSTRAP_PEERS` and the embedded seed list are
unaffected, since those are HTTP URLs and carry no PeerId.

---

## Optional node staking
Expand Down
1 change: 1 addition & 0 deletions crates/gitlawb-node/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ sqlx = { version = "0.8", features = ["postgres", "runtime-tokio-rustls", "chron
clap = { version = "4", features = ["derive", "env"] }
bytes = "1"
libc = "0.2"
zeroize = "1"
cid = { workspace = true }
hex = { workspace = true }
sha2 = { workspace = true }
Expand Down
Loading
Loading