pub-repo
Een minimale git-remote voor je eigen site. Je pusht met één token, en zet per repository met één knop of hij publiek zichtbaar en anoniem te clonen is.
Geen accounts, geen issues, geen pull requests, geen CI. Wel: echte git clone
en git push over HTTPS, en een bladerbare weergave van je publieke code.
Wat het doet
| Pushen | Alleen jij, met één owner-token via HTTPS Basic auth |
| Publieke repo | Anoniem te clonen én te bladeren op je site — als momentopname |
| Privé repo | Onzichtbaar: geeft exact hetzelfde antwoord als een repo die niet bestaat |
| Bladeren | Bestandsboom, bestandsinhoud met syntax highlighting, README-rendering, branchkeuze |
| Beheer | /admin: repo aanmaken, beschrijving zetten, publiek/privé omzetten, verwijderen |
Bewust weggelaten: issues, forks, pull requests, meerdere gebruikers — en commitgeschiedenis, zie hieronder.
Geen geschiedenis
Wat je publiceert is de inhoud, niet hoe je eraan gekomen bent. Een anonieme clone levert per branch één commit zonder ouders op: alle bestanden, geen verleden. Jouw eigen geschiedenis blijft wél op de server staan en haal je terug via een tweede transport dat altijd om je token vraagt.
git push -> <naam>.git jouw repository, volledige geschiedenis
| bereikbaar via /git-full (token vereist)
momentopname
v
.published/<naam>.git losse repository, 1 commit per branch
bereikbaar via /git (publiek)
Dat zijn twee aparte repositories op schijf, en dat is geen overdaad. Binnen
één repository valt dit niet af te schermen: transfer.hideRefs en git-namespaces
houden refs alleen uit de advertentie, waarna een client de objecten alsnog met
git fetch <sha> ophaalt. De handleiding van git zegt het zelf, in
gitnamespaces(7):
The fetch and push protocols are not designed to prevent one side from stealing data from the other repository that was not intended to be shared. If you have private data that you need to protect from a malicious peer, your best option is to store it in another repository.
Dus staat het in een andere repository. Wat er niet in zit, kan er niet uit.
Omdat de momentopname geen ouders heeft, gaan bij het publiceren alleen de boom en de bestanden mee. Een push die niets aan de inhoud verandert levert geen nieuwe momentopname op, dus het gepubliceerde commit-id blijft dan stabiel. Oude momentopnames worden direct opgeruimd — ook die zijn geschiedenis.
Hoe het werkt
Repositories zijn gewone bare git-repos onder REPO_ROOT, als <naam>.git, met
de gepubliceerde momentopnames in REPO_ROOT/.published/. Het git-protocol zelf
wordt afgehandeld door git http-backend — het CGI-programma dat git meelevert.
Deze applicatie doet daar de toegangscontrole voor, en leest voor de webweergave
de objecten uit met gewone git-commando's.
Welke van de twee repositories http-backend te zien krijgt, is simpelweg een
kwestie van welke map als GIT_PROJECT_ROOT wordt meegegeven. Anoniem ophalen
kan de repositories met geschiedenis dus niet eens zien staan.
Je data zit nooit gevangen: REPO_ROOT is een map met bare repos die je kunt
rsyncen, backuppen of ergens anders neerzetten. De enige extra informatie is per
repo een pub-repo.json met de zichtbaarheid en beschrijving.
src/
index.ts server, middleware-volgorde, security headers
config.ts omgevingsvariabelen
auth.ts owner-token, Basic auth, sessiecookie
repos.ts repos op schijf: aanmaken, metadata, lijst
git.ts veilige wrappers om git-commando's
routes/smart-http.ts toegangscontrole + git http-backend
routes/web.ts publieke weergave
routes/admin.ts inloggen en beheer
views.ts HTML-rendering
public/styles.css styling, licht en donker
Lokaal draaien
Vereist Node 22+ (draait TypeScript rechtstreeks) en git in PATH.
npm install
cp .env.example .env # vul OWNER_TOKEN en SESSION_SECRET in
npm run dev
Genereer de twee geheimen met openssl rand -hex 32 (of
node -e "console.log(require('crypto').randomBytes(32).toString('hex'))").
Daarna: ga naar /login, log in met je OWNER_TOKEN, maak op /admin een repo
aan en push ernaartoe.
Uitrollen
deploy/setup.sh doet de provisionering op een Debian- of Ubuntu-box met nginx en certbot: systeemgebruiker, mappen, een bare deploy-repo met post-receive hook, verse geheimen, een systemd-unit, een certificaat en de vhost. Het script is idempotent.
De unit en de vhost komen uit de repo zelf, dus die zijn er pas na de eerste push. Vandaar twee rondes:
export DOMAIN=git.voorbeeld.nl LE_EMAIL=jij@voorbeeld.nl
# 1. gebruiker, mappen, deploy-repo, .env met verse geheimen, certificaat
ssh root@server 'bash -s' < deploy/setup.sh
# 2. code erheen
git remote add production ssh://root@server/srv/git/pub-repo.git
git push production main
# 3. unit en vhost installeren, service starten
ssh root@server 'bash -s' < deploy/setup.sh
Daarna deployt elke git push production main zichzelf via de post-receive hook:
checkout bijwerken, npm ci --omit=dev, service herstarten.
Wat waar staat
| Pad | Wat |
|---|---|
/opt/pub-repo |
de code, een git-checkout van /srv/git/pub-repo.git |
/opt/pub-repo/.env |
de geheimen, chmod 600 |
/var/lib/pub-repo/repos |
je repositories met geschiedenis — dit is je data |
/var/lib/pub-repo/repos/.published |
de momentopnames; volledig afleidbaar uit het bovenstaande |
/srv/git/pub-repo.git |
deploy-remote, staat los van je gepubliceerde repos |
Twee valkuilen
Beide zitten in het script verwerkt, maar als je het met de hand doet:
nginx. client_max_body_size 0 en proxy_request_buffering off op de
git-locatie zijn niet optioneel. Een push is één grote POST; met de
standaardlimiet van 1 MB mislukt elke push van enige omvang, en zonder de tweede
schrijft nginx de hele pack eerst naar schijf.
safe.directory. De bare deploy-repo is van root, de fetch in de hook draait
als pubrepo. Zonder onderstaande regel faalt elke deploy met
detected dubious ownership:
sudo -u pubrepo -H git config --global --add safe.directory /srv/git/pub-repo.git
Het owner-token gaat als Basic auth over de lijn, dus draai dit nooit zonder TLS.
Gebruik
Maak de repo eerst aan op /admin, en koppel hem dan vanaf je werkmachine:
git remote add origin https://git.voorbeeld.nl/git/mijn-project.git
git push -u origin main
Git vraagt om een gebruikersnaam en wachtwoord. De gebruikersnaam maakt niet uit
(vul bijvoorbeeld owner in), het wachtwoord is je OWNER_TOKEN. Wil je niet
elke keer typen, laat git het onthouden:
git config --global credential.helper store # of: osxkeychain, manager
Zichtbaarheid omzetten doe je op /admin — de knop toont de huidige stand en
zet hem om. Publiek gemaakte repos verschijnen direct op de voorpagina.
Wil je je eigen geschiedenis terughalen, bijvoorbeeld omdat je lokale kopie weg is, gebruik dan het volledige transport. Dat vraagt altijd om je token, ook bij een publieke repository:
git clone https://git.voorbeeld.nl/git-full/mijn-project.git
Beveiliging
Wat er is afgedekt, zodat je weet waar je op moet letten:
- Privé is echt privé. Een privé-repo en een niet-bestaande repo geven hetzelfde antwoord, zowel op het web als op het git-endpoint. Er lekt dus geen lijst met projectnamen.
- Geschiedenis komt er niet uit, ook niet per object-id, omdat ze in een andere repository staat dan wat publiek geserveerd wordt.
- De blader-UI leest alleen de gepubliceerde repository, ook als jij zelf ingelogd bent. Je ziet dus precies wat een bezoeker ziet, en een commit-sha in de URL levert een 404 op in plaats van een pagina.
- Pushen vereist altijd het token, ook naar publieke repos, en dat wordt gecontroleerd voordat de server kijkt of de repo bestaat.
- Geen padtraversal. Alleen drie exacte URL-vormen bereiken
git http-backend; repo-namen en refs gaan door een strikt patroon. - Ruwe bestanden worden altijd als
text/plainofapplication/octet-streamgeserveerd, metnosniffen een sandbox-CSP, zodat HTML of JS uit een repository niet als script op je eigen domein kan draaien. - README's worden gerenderd zonder ruwe HTML toe te staan.
- Sessiecookie is HttpOnly, SameSite=Lax en ondertekend met een eigen geheim; POST-verzoeken van andere origins worden geweigerd.
Eén ding om te weten: als je een typefout maakt in de naam van een publieke repo, vraagt git om een wachtwoord in plaats van te zeggen dat hij niet bestaat. Dat is de prijs voor het niet lekken van privé-namen.
Back-up
rsync -a root@server:/var/lib/pub-repo/repos/ ./repos-backup/ # incl. .published/
Meer is er niet. Er is geen database.