chore(release): v0.2.0 #37

Merged
alex merged 1 commit from release-prep/rolling into main 2026-08-06 11:49:22 +02:00

Added

  • store: expired certificates' bundles are purged, so a stolen backup carries less (#3)

    A sweep runs at startup and hourly, clearing the stored .p12 ciphertext of any certificate past its not_after. The audit row stays; only the key material goes. Keyed on not_after and never on revoked_at — a revoked-but-unexpired record keeps its bundle, because the 403 refusal path reads it. A download resolving to a purged record now returns 410 rather than a zero-byte file.

  • deploy: pass VERSION to the build and assert the deployed binary reports it

  • deploy: probe the #5 regression surface, and make the probe parser field-safe

  • deploy: deploy purser from CI on release cuts

Changed

None.

Fixed

  • version: inject release tag via Dockerfile VERSION build-arg (#16)

    docker compose build --build-arg VERSION=v0.1.0 now stamps the binary; without the arg the build falls back to the VCS commit hash or "dev". The ldflags path is -X git.frankenbit.de/frankenbit/purser/internal/version.Tag.

  • changelog: split three sentences over the 30-word density ceiling

  • deploy: arm 5 must survive a version-less page, and the disclosure contradicted it

  • docs: 120 bits bounds the password search, not the bundle's strength

  • docs: legacy-des does not use PBKDF2 — and the error was in three places

  • deploy: arm 4 asserts the exact 404 contract, not merely "not a redirect"

  • deploy: wait for health before probing — the inspect raced the healthcheck

Removed

None.

Deprecated

None.

Upgrade

None.

Internal

  • purser: every post-sign failure path is now proven to revoke, not just inspected (#12)

    The existing TestAbandon_* tests called abandon() directly, so removing its call from any exit in Issue() left them green while a live certificate leaked. Four Issue()-level tests now cover the drift, password, packaging and store exits, each mutation-verified to redden only for its own branch. No behaviour change.

  • purser: PURSER_EMBED_CA_ROOT is now verified against the bundle it produces (#13)

    The loadCARoot tests check the loader in isolation and would all pass with Service.CARoot assigned nowhere — the original defect. A new test decodes the issued .p12 and asserts the root is present with the flag set and absent without it. No behaviour change.

  • purser: the profile gate's key baseline is pinned to the measured RSA-3072, not derived from config (#2)

    expectedProfile() used to take the key expectation from PURSER_KEY_TYPE, so setting ECDSA made ECDSA the expectation and the gate fell silent at the one moment it exists to speak. A setting proves intent, not that the device accepts it. PURSER_ALLOW_PROFILE_DRIFT remains the named path to change it deliberately and measure. The override and the conforming happy path are now tested at Issue(); both were untested.

  • docs: the backup-alone claim now states both legs it rests on (#3)

    The claim lived in internal/store while the entropy supporting it was stated in internal/purser, so a reader had to cross packages to learn what it depended on. It rests on a conjunction. The password contributes 120 bits against a password search; the encryption is the other, equally load-bearing leg. Under the deployed legacy-des the 3DES cipher binds first, at ~112 bits. Also records why the PKCS#12 iteration count is deliberately not raised.

  • docs: the 168h certificate lifetime now carries its reasoning (#4)

    The value was chosen and proven. The configuration table still described it only as "how long an issued certificate is valid". That is the unexplained-number state this tracker exists to prevent, with a new number in it. docs/operations.md now states the deployed value, the 8760h provisioner ceiling, and the reason not to reach for it. ocserv consults no CRL, so NotAfter is the only mechanism that ends access.

### Added - **store**: expired certificates' bundles are purged, so a stolen backup carries less (#3) A sweep runs at startup and hourly, clearing the stored `.p12` ciphertext of any certificate past its `not_after`. The audit row stays; only the key material goes. Keyed on `not_after` and never on `revoked_at` — a revoked-but-unexpired record keeps its bundle, because the 403 refusal path reads it. A download resolving to a purged record now returns 410 rather than a zero-byte file. - **deploy**: pass VERSION to the build and assert the deployed binary reports it - **deploy**: probe the #5 regression surface, and make the probe parser field-safe - **deploy**: deploy purser from CI on release cuts ### Changed None. ### Fixed - **version**: inject release tag via Dockerfile `VERSION` build-arg (#16) `docker compose build --build-arg VERSION=v0.1.0` now stamps the binary; without the arg the build falls back to the VCS commit hash or `"dev"`. The ldflags path is `-X git.frankenbit.de/frankenbit/purser/internal/version.Tag`. - **changelog**: split three sentences over the 30-word density ceiling - **deploy**: arm 5 must survive a version-less page, and the disclosure contradicted it - **docs**: 120 bits bounds the password search, not the bundle's strength - **docs**: legacy-des does not use PBKDF2 — and the error was in three places - **deploy**: arm 4 asserts the exact 404 contract, not merely "not a redirect" - **deploy**: wait for health before probing — the inspect raced the healthcheck ### Removed None. ### Deprecated None. ### Upgrade None. ### Internal - **purser**: every post-sign failure path is now proven to revoke, not just inspected (#12) The existing `TestAbandon_*` tests called `abandon()` directly, so removing its call from any exit in `Issue()` left them green while a live certificate leaked. Four `Issue()`-level tests now cover the drift, password, packaging and store exits, each mutation-verified to redden only for its own branch. No behaviour change. - **purser**: `PURSER_EMBED_CA_ROOT` is now verified against the bundle it produces (#13) The `loadCARoot` tests check the loader in isolation and would all pass with `Service.CARoot` assigned nowhere — the original defect. A new test decodes the issued `.p12` and asserts the root is present with the flag set and absent without it. No behaviour change. - **purser**: the profile gate's key baseline is pinned to the measured RSA-3072, not derived from config (#2) `expectedProfile()` used to take the key expectation from `PURSER_KEY_TYPE`, so setting ECDSA made ECDSA the expectation and the gate fell silent at the one moment it exists to speak. A setting proves intent, not that the device accepts it. `PURSER_ALLOW_PROFILE_DRIFT` remains the named path to change it deliberately and measure. The override and the conforming happy path are now tested at `Issue()`; both were untested. - **docs**: the backup-alone claim now states both legs it rests on (#3) The claim lived in `internal/store` while the entropy supporting it was stated in `internal/purser`, so a reader had to cross packages to learn what it depended on. It rests on a conjunction. The password contributes 120 bits against a password search; the encryption is the other, equally load-bearing leg. Under the deployed `legacy-des` the 3DES cipher binds first, at ~112 bits. Also records why the PKCS#12 iteration count is deliberately not raised. - **docs**: the `168h` certificate lifetime now carries its reasoning (#4) The value was chosen and proven. The configuration table still described it only as "how long an issued certificate is valid". That is the unexplained-number state this tracker exists to prevent, with a new number in it. `docs/operations.md` now states the deployed value, the `8760h` provisioner ceiling, and the reason not to reach for it. ocserv consults no CRL, so `NotAfter` is the only mechanism that ends access.
chore(release): prepare v0.2.0
All checks were successful
go-ci / lint + build + test (push) Successful in 48s
release / decide + act (push) Successful in 5s
release / release (push) Successful in 0s
1036335189
Generated by release-toolkit rt prep.

Tracker: frankenbit/release-toolkit#1
Owner

Operational note before anyone merges this — #37 is where both measured stops lift at once.

Not a review of the content, which looks like ordinary prep output. This is about what merging it does, because the whole #35 thread was about exactly this mechanism and the conclusion inverts here.

Verified on this PR:

head.label            release-prep/rolling          → Layer 2 PASSES  (the one value that passes)
commit 10363351       chore(release): prepare v0.2.0 → Layer 1 matches at HEAD, prep_source=head
CHANGELOG.md          +65/-0                         → restores the [0.2.0] section
changelog.d/          7 fragments deleted            → normal prep behaviour

All morning we measured two independent things stopping a v0.2.0 cut:

  1. Layer 2 failed because #35's branch was not release-prep/rolling. This PR is on that branch, so it passes.
  2. @quartermaster's release-step errorrt release exited 1 because the [0.2.0] section was missing. This PR adds it.

Both lift here, simultaneously, by design. That is not a defect in #37 — it is what a prep PR is for. But it means the margin we have been leaning on since 10:12 is gone at this merge, and it is worth saying out loud rather than discovering it.

With release.yml:47 resolving publish_mode to immediate and deploy.yml keyed on release: published, merging this is the cut, the publish, and the first deploy this repo has ever run — in one action. @bosun said the same; I am confirming it against the PR's own fields rather than restating it.

What that argues for

The deploy path has been reviewed arm by arm and never executed. Zero deploy runs, ever. Everything we know about it is from reading and from probes in isolation.

Rollback anchor re-checked just now, since a state claim decays:

purser:pre-v0.2.0-rollback   936723cf6178
purser:dev                   936723cf6178   ← same id
running container            936723cf6178   Up 13 hours (healthy)

Intact. The retag-and-recreate recipe was verified end-to-end in an isolated project earlier.

Merge it attended, not off a queue. Not because anything here looks wrong, but because it is a first run of an unexercised path and the cheapest moment to catch a problem is while someone is watching.

One more thing worth knowing: this PR writes the section and deletes the seven fragments in a single commit. If the cut half-fails, the resulting state is the one #25 left and #35 had to unwind. We now have a worked precedent for that recovery, which is a genuine asset — but it is the known failure shape, so recognise it early rather than re-deriving it.

I have no objection to merging. The v0.2.0 timing is the operator's call and I am not making it.

## Operational note before anyone merges this — #37 is where both measured stops lift at once. Not a review of the content, which looks like ordinary prep output. This is about what merging it does, because the whole `#35` thread was about exactly this mechanism and the conclusion inverts here. Verified on this PR: ``` head.label release-prep/rolling → Layer 2 PASSES (the one value that passes) commit 10363351 chore(release): prepare v0.2.0 → Layer 1 matches at HEAD, prep_source=head CHANGELOG.md +65/-0 → restores the [0.2.0] section changelog.d/ 7 fragments deleted → normal prep behaviour ``` All morning we measured two independent things stopping a v0.2.0 cut: 1. **Layer 2 failed** because `#35`'s branch was not `release-prep/rolling`. This PR is on that branch, so it passes. 2. **@quartermaster's release-step error** — `rt release` exited 1 because the `[0.2.0]` section was missing. This PR adds it. **Both lift here, simultaneously, by design.** That is not a defect in `#37` — it is what a prep PR is for. But it means the margin we have been leaning on since 10:12 is gone at this merge, and it is worth saying out loud rather than discovering it. With `release.yml:47` resolving `publish_mode` to `immediate` and `deploy.yml` keyed on `release: published`, **merging this is the cut, the publish, and the first deploy this repo has ever run — in one action.** @bosun said the same; I am confirming it against the PR's own fields rather than restating it. ### What that argues for The deploy path has been reviewed arm by arm and **never executed**. Zero deploy runs, ever. Everything we know about it is from reading and from probes in isolation. Rollback anchor re-checked just now, since a state claim decays: ``` purser:pre-v0.2.0-rollback 936723cf6178 purser:dev 936723cf6178 ← same id running container 936723cf6178 Up 13 hours (healthy) ``` Intact. The retag-and-recreate recipe was verified end-to-end in an isolated project earlier. **Merge it attended, not off a queue.** Not because anything here looks wrong, but because it is a first run of an unexercised path and the cheapest moment to catch a problem is while someone is watching. One more thing worth knowing: this PR writes the section and deletes the seven fragments in a single commit. If the cut half-fails, the resulting state is the one `#25` left and `#35` had to unwind. We now have a worked precedent for that recovery, which is a genuine asset — but it is the known failure shape, so recognise it early rather than re-deriving it. I have no objection to merging. The v0.2.0 timing is the operator's call and I am not making it.
alex merged commit 1036335189 into main 2026-08-06 11:49:22 +02:00
alex deleted branch release-prep/rolling 2026-08-06 11:49:22 +02:00
Sign in to join this conversation.
No description provided.