Voer DEPLOY-3.1.md uit als een script, met bewijs per stap - #352
Merged
Conversation
deploy/deploy-3.1.sh draait het runbook vanaf de NUC in acht fasen: de controles vooraf, layout en env, rollback-tags en backups, pull en build, de recreate met relay-iot als kanarie, frontend en nginx, de smoketests en de rollback. Elke stap print de kop, het commando en de meting, en stopt bij het eerste resultaat dat het runbook niet voorspelt: een checkout die niet op 41501bb staat, een container die niet healthy wordt, een billing_config-regel zonder recurring:false en mode_source:inferred, een relay_started zonder version 3.1.0, of een static-sanity met een FAIL buiten check 10. Alles op de server gaat via een ssh-heredoc met BatchMode en IdentitiesOnly. Sleutels worden nooit geprint, alleen presence, lengte en een prefix van vijf tekens. Elke fase die iets kapot kan maken print een bewijsregel voor en na. --preflight-only doet fase 0 en 1 en stopt voor de eerste schrijfactie. --rollback <TS> zet de getagde images, .env, de nginx-confs en de docroot terug. --dry-run print elk remote commando in plaats van het uit te voeren. tests/deploy-3.1-dryrun.test.sh draait die dry-run en controleert dat elke fase wordt bereikt, dat elke modus stopt waar hij moet stoppen, en dat geen enkele regel een sleutelwaarde kan bevatten. De scan strippt de drie vormen die het runbook wel toestaat (lengte, prefix van vijf, een header die curl opeet) en een write die in .env belandt, en meldt wat er dan nog overblijft. Het runbook blijft de leesbare bron; het script voert het uit.
Apolloccrypt
force-pushed
the
feat/deploy-3.1-script
branch
from
September 2, 2026 16:46
9fdd341 to
facccb8
Compare
De ssh-argumenten waren de echte bug. ssh bewaart argv niet: het plakt alles na de host aan elkaar tot een string en de remote shell splitst opnieuw op witruimte. Zes servicenamen als een argument kwamen daardoor aan als zes parameters, dus elke remote lus over $2 draaide een keer: fase 1 keek naar 1 van de 6 containers en fase 2 schreef een manifest van 1 regel. q_args() quote elk argument nu met printf %q. De test bootst de samenvoeging na met bash -c en heeft een tegenproef: zonder quoting zakt hij terug naar 1 van 6. Verder: - alle twintig heredocs draaien set -euo pipefail, cp en chmod staan los - remote() vangt de exitcode, print de serveruitvoer en stopt met diagnose - backupasserties meten bytes, tarentries en het aantal docker-tags, niet een geechode padstring - fase 5 bewerkt alleen de twee benoemde confs, volgt symlinks met readlink, eist per sed-edit dat er iets te doen valt en stopt als de serverconf afwijkt van het patroon dat het runbook noemt - fase 5 verwijdert de 26 frontendbestanden die main weghaalde, afgeleid uit git diff --diff-filter=D en niet uit een wildcard; anders serveert try_files /compliance/nis2 en /paraid gewoon door. Fase 6 controleert dat ze 404 geven - --preflight-only schrijft niets meer: 1c en 1d melden alleen - rollback zet .env terug voor de recreate, test nginx voor de reload, ruimt op wat de deploy toevoegde en stopt als een backup ontbreekt - de secretscan is een taintscan geworden: hij leidt uit de bron af welke variabelen een .env-waarde dragen en meldt elke echo daarvan, dus een hernoeming glipt er niet meer langs Runbook: de SDK-precondition is 3.3.0, niet 3.2.1, en die release komt niet op PyPI omdat er geen trusted publisher staat. De deploy wacht er niet meer op en neemt de eerste uitweg: PARAMANT_INLINE_RECEIPT_HEADER=1 in .env, de drie proxy_buffer-regels op /v2/outbound, en een smoketest die een echte download doet en eist dat het headerblok boven 16 KB uitkomt zonder 502. Zonder PARAMANT_SMOKE_API_KEY zegt die stap wat hij niet heeft bewezen.
docker-compose.yml heeft geen env_file. .env vult alleen ${VAR} in het
compose-bestand zelf in, dus een variabele zonder eigen regel in x-relay-env
bereikt nooit een container. De vier receipt-variabelen uit #342 hadden die
regel niet, dus fase 1d schreef een vlag die nergens aankwam en fase 6h zou
pas aan het eind sterven, met 3.1.0 al live.
PARAMANT_INLINE_RECEIPT_HEADER en de drie PARAMANT_RECEIPT_-variabelen staan
nu in x-relay-env, allemaal met een lege default: de vlag telt alleen bij
exact 1 en de caps vallen leeg terug op hun ingebouwde getallen. Bewezen met
docker compose config: met de vlag in .env rendert hij op alle vijf de
relays, zonder de vlag op geen enkele.
Fase 4 controleert dat nu op de server voordat er iets herstart wordt, zodat
een ontbrekende doorgifte blokkeert op een leesbare assertie in plaats van op
een smoketest achteraf. Runbook: de regel dat compose byte-identiek is aan
41501bb klopte al niet meer sinds #340 en is vervangen door wat er nu speelt.
Verder uit ronde 2:
- de bufferguard telt binnen elk /v2/outbound-blok in plaats van per bestand.
Een conf met proxy_buffer_size op /v2/inbound liet het outbound-blok leeg;
nagespeeld en gedicht met een tweetraps-awk die ook idempotent is
- fase 5b resolvet het doelpad met readlink -f en weigert alles dat buiten
de docroot uitkomt, met een teller die geassert wordt
- 6h claimt de 16 KB alleen als het headerblok er echt boven zit; anders
WARN met NOT PROVEN en geen ok-regel, zodat de samenvatting niet suggereert
dat het bewezen is
- de taintscan erft over een toewijzing, dus s3cr="$tok" telt ook. Een hop,
bewust, en die grens staat in de test opgeschreven
This was referenced Sep 2, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
deploy/deploy-3.1.shvoertdeploy/DEPLOY-3.1.mduit. Een deploy is eencommando vanaf de NUC:
Het runbook blijft de leesbare bron. Het script voegt niets aan het plan toe,
het weigert alleen door te gaan als een meting het tegenspreekt.
Reviewronde: negen punten, met bewijs
1. De ssh-argumenten. Dit was een echte bug en de reviewer had gelijk.
ssh bewaart argv niet: het plakt alles na de host aan elkaar tot een string, en
de remote shell splitst opnieuw op witruimte. De zes servicenamen als een
argument kwamen aan als zes losse parameters, dus elke remote lus over
$2draaide precies een keer. Reproductie, met
bash -cals sshd:q_args()quote nu elk argument metprintf %q. De test bootst desamenvoeging na en heeft een tegenproef die faalt als de simulatie ooit stopt
met ssh na te doen. Er draait geen sshd waar ik een sleutel voor heb, dus de
simulatie is
"$*"plusbash -c, wat exact is wat de client en de serversamen doen.
2. Strict mode. Alle twintig heredocs draaien
set -euo pipefail; de testtelt heredocs tegen strict-regels, dus een nieuwe heredoc zonder
-efaalt.cp ... && chmod ...is uit elkaar getrokken (een AND-lijst verbergt eenfalende
cpvoor-e), en de test verbiedt dat patroon.remote()vangt deexitcode, print de serveruitvoer met
|-prefix en stopt daarna met deSTOP-regel, dus de diagnose staat er nog.
3. Backupasserties meten nu iets.
after .env backup bytes,after docroot tar bytes,after docroot tar entries, enafter tags for this TS = 6telt de echtedocker images-tags in plaats vande zes regels tekst in het manifest.
expect_countenexpect_minlezen eenveld en vergelijken een getal; ze matchen niet meer op een geechode padstring.
4. Fase 5c. Alleen de twee benoemde confs (
PARAMANT_NGINX_CONFS),readlink -feerst wantcp -avan een symlink is geen backup, en persed-edit de eis dat
before > 0. Is er niets te doen, dan stopt hij metFATAL nothing to change for 'sign': de server conf heeft niet de vorm die het runbook noemtin plaats van OK te melden.after edited files = 2is eenharde assertie.
5.
--preflight-onlyschrijft niets. 1c en 1d krijgenreportin plaatsvan
writemee en melden alleen. De slotregel isNothing was written, defaseheader zegt
read-only, en het runbook zegt het ook. De test controleertdat preflight
bash -s -- ... reportaanroept en nergenswrite.6. Rollback.
.envgaat terug voor de recreate, zodat de containers deoude file lezen.
nginx -tvoor de reload, en bij een fout geen reload.tar xlegt over maar verwijdert niet wat de deploy toevoegde, dus 8cvergelijkt de tar-inhoud met de docroot en ruimt het verschil op (met de
IGNORE-lijst ontzien). 8a stopt als het manifest,
.env, de tar of eennginx-backup ontbreekt:
missing backups = 0is een assertie, geen mededeling.7. De 26 verwijderde frontendbestanden.
rsynczonder--deleteliet zestaan en
try_filesbleef ze serveren, dus/compliance/nis2en/paraidgaven nog 200. Fase 5b leidt de lijst af uit
git diff --diff-filter=D --name-only <vorige HEAD>..HEAD -- frontend/(26bestanden,
frontend/paraid.htmlerbij) en verwijdert die uit de docroot, metde IGNORE-lijst van
check-prod-drift.shontzien en een pad-guard. Fase 6econtroleert dat
/compliance/nis2,/compliance/iec62443,/compliance/nen7510,/paraid,/paraid-appen/dicom404 geven.8. De SDK-precondition. Het runbook zei 3.2.1; dat is 3.3.0, en die release
komt niet op PyPI omdat er geen trusted publisher staat (gerapporteerd
2026-09-02, niet hier gemeten; zo staat het er ook). De deploy wacht er niet
meer op en neemt de eerste uitweg uit het runbook:
PARAMANT_INLINE_RECEIPT_HEADER=1in.envals de regel ontbreekt (fase 1d),de drie
proxy_buffer-regels op elkelocation ~ ^/v2/outbound(fase 5c,idempotent, met
before outbound locations > 0als eis), en fase 6h. Die leestde env-var uit de draaiende containers, leest
nginx -Tin plaats van hetbestand, controleert dat
/v2/outboundgeen 5xx geeft, en doet metPARAMANT_SMOKE_API_KEYeen echte upload plus download tegenrelay.paramant.app: 200 en geen 502,X-Paramant-Receiptaanwezig, de driereferentievelden aanwezig, en het headerblok boven 16 KB. Zonder die sleutel
zegt de stap met zoveel woorden wat hij niet heeft bewezen. Het runbook
noemt nu ook de vervolgstap: zodra 3.3.0 op PyPI staat, de regel eruit en
recreaten.
9. De secretscan is een taintscan geworden. Een namenlijst vangt alleen de
namen die erop staan. De test leidt nu uit de bron af welke variabelen een
.env-waarde of eenopenssl randdragen (nu:miss T tok) en meldt elkeechoofprintfdaarvan. Tellende lezen (grep -c,wc -l) leveren eengetal en tellen niet mee. Drie tegenproeven:
cat .envecho "deep token IS $T"SEKRIT="$(grep '^ADMIN_TOKEN=' .env | cut -d= -f2-)"; echo "leaked $SEKRIT"Reviewronde 2: de blocker en vier guards
1. Blocker: de vlag bereikte de containers nooit.
docker-compose.ymlheeft geen
env_file..envvult alleen${VAR}in het compose-bestandzelf in, dus een variabele zonder eigen regel in
x-relay-envkomt nooit ineen container, hoe netjes je hem ook in
.envzet. De vier receipt-variabelenuit #342 hadden die regel niet. Fase 1d schreef dus een vlag die nergens
aankwam, en fase 6h zou pas aan het eind falen met 3.1.0 al live.
Toegevoegd aan
x-relay-env, alle vier met lege default (de vlag telt alleenbij exact
1; de caps vallen leeg terug op hun ingebouwde getallen):Bewijs, lokaal met
docker compose config:Vijf relays plus het anker. Nieuwe stap 4pre doet exact deze meting op de
server voordat er iets herstart wordt en eist
>= 5, dus een ontbrekendedoorgifte blokkeert op een leesbare assertie in plaats van op een smoketest
achteraf. Runbookregel 52-54 herschreven: de claim dat compose byte-identiek
is aan
41501bbklopte trouwens al niet meer sinds #340 (git diffgeeft2 insertions, 1 deletion), en er staat nu bij dat fase 3 het compose-bestand
meetrekt en fase 4 het oppakt.
2. Bufferguard per blok in plaats van per bestand. Het gat nagespeeld:
Vervangen door een tweetraps-awk: pass 1 leest per outbound-blok of de buffer
er al staat, pass 2 voegt alleen toe waar hij ontbreekt. Op de conf hierboven
belandt hij nu wel in het outbound-blok, en tweemaal draaien verandert niets
(2 bufferregels blijven 2,
cmpgelijk). De eindcheck telt ook per blok:after outbound blocks with buffermoet gelijk zijn aanafter outbound locations.3. Fase 5b resolvet het doelpad.
readlink -fop het te verwijderenbestand en op de docroot, en alles wat niet echt onder de docroot uitkomt
wordt geweigerd en geteld.
after refused outside docroot = 0is een hardeassertie, dus een symlinked submap kan
rmniet meer buiten de docroot latenkomen.
4. De 16 KB-claim alleen als hij waar is. De
okover het oversizedheaderblok staat nu in de
> 16000-tak. Onder die grens volgt een WARN metNOT PROVENen geen enkeleok, dus de samenvatting kan niet lezen alsof hetpad door nginx bewezen is terwijl het dat niet is.
5. Taint erft over een toewijzing.
s3cr="$tok"telt nu ook als besmet.Tegenproef:
s3cr="$T"; echo "inherited $s3cr"wordt gevangen. Een hop, endie grens staat als commentaar in de test, met de reden erbij.
Lokaal groen, ronde 2
bash -nschoon op beide bestanden.tests/static-sanity.sh: PASS, alle tien checks OK, check 10 groen.tests/deploy-3.1-dryrun.test.sh: 108 pass, 0 fail.docker compose configmet en zonder de vlag: 6 tegen 0 (hierboven).--dry-run.Lokaal groen
bash -nschoon op beide bestanden;shellcheckstaat niet op deze machineen wordt overgeslagen als hij ontbreekt.
tests/static-sanity.sh: PASS, alle tien checks OK, check 10 groen.tests/deploy-3.1-dryrun.test.sh: 96 pass, 0 fail.--dry-run, geen enkele ssh gelegd.Dry-run uitvoer, ingekort
Bewust niet geautomatiseerd
vaultnotitie. Elk vraagt een oordeel of een geheim dat het script niet maakt.
BILLING_MODEzetten. Precies de deploy waar de rem voor bestaat. Hetscript controleert dat hij leeg is en zet hem nooit.
telt hem voor en na en breekt af als hij verdwijnt.
WARN met de lijst erbij; alleen Mick weet of zo'n bestand mag sneuvelen.
account, en een deploy hoort geen productiesleutel te bevatten. Met
PARAMANT_SMOKE_API_KEYdraait hij, zonder zegt hij dat hij het niet weet.scripts/rollback-3.0.0.shwordt niet aangeroepen. Die vraagtinteractief om bevestiging, wat niet gaat door
ssh 'bash -s'. Fase 8 doetdezelfde stappen plus de nginx- en docroot-restore die dat script niet doet.