Skip to content

Releases: jfudally/wg_manager

v0.5.0

Choose a tag to compare

@github-actions github-actions released this 24 Jun 19:08
c5a0438

Added

  • scripts/wg_bootstrap.sh — VPN-first node bootstrap. A multipurpose
    orchestrator-run script that enrolls a node from the operator
    workstation: vpn joins the node to the WireGuard VPN by calling the
    control plane directly (POST /<ver>/clients/manual over mTLS), pushes
    the rendered wg0.conf to the node, and brings the tunnel up; cinc
    runs knife bootstrap over the now-up VPN; all does both, handing the
    assigned VPN IP from the first phase to the second. The tunnel teardown
    before rewriting the config preserves the reprovision-safety contract.
    Shape tests in tests/test_wg_bootstrap_script.py.

Changed

  • Peer discovery now clears stale results instead of accumulating
    ghosts.
    Each successful discovery pass is treated as authoritative
    for the server: any DiscoveredPeer row whose public key was not
    observed in the pass is pruned, so a peer removed from a server's
    running config disappears from the table on the next run rather than
    lingering with an old last_seen_at. Pruning runs only after a
    successful wg show — an unreachable host returns early and never
    wipes the known peer set. The discover_peers_task result gains a
    pruned count, and the Discovered Peers dashboard clears its
    displayed list the moment a "Discover all servers" run is dispatched
    so the table never shows a previous pass's peers while a new run is
    in flight. Regression tests in tests/test_discovery.py and
    web/__tests__/discovered-peers.test.tsx.

Removed

  • The wg_node Cinc cookbook (cookbooks/wg_node/). It brought a node
    onto the VPN by converging against the Cinc server first, which forced
    the Cinc server to be reachable before the node had any VPN connectivity
    — i.e. public-facing. scripts/wg_bootstrap.sh (above) replaces it by
    flipping the order: VPN first, then Cinc over the tunnel, so the Cinc
    server can stay VPN-only/private. The cookbook-test / cookbook-lint
    Makefile targets are dropped with it.

Published images

  • ghcr.io/jfudally/wg_manager:0.5.0 — API + worker
  • ghcr.io/jfudally/wg_manager-web:0.5.0 — dashboard

Pull either with a granularity-appropriate tag (0.5.0, or a coarser 0.5 / 0 / latest).

Supply-chain attestation

Both images are signed via cosign keyless OIDC and carry an
in-toto CycloneDX SBOM attestation. Verify with:

cosign verify \
    --certificate-identity-regexp \
        'https://github.com/jfudally/wg_manager/.github/workflows/release.yml@.*' \
    --certificate-oidc-issuer \
        'https://token.actions.githubusercontent.com' \
    ghcr.io/jfudally/wg_manager:0.5.0

The SBOMs are also attached as release assets:

  • sbom-api.cdx.json — CycloneDX SBOM of the API + worker image's Python deps
  • sbom-web.cdx.json — CycloneDX SBOM of the dashboard image's Node deps

v0.4.0

Choose a tag to compare

@github-actions github-actions released this 19 Jun 17:57
fbf3ee4

Fixed

  • SSH_CA_VAULT_ALLOWED_USERS / SSH_CA_VAULT_ALLOWED_HOST_DOMAINS
    now actually take effect from .env.prod.
    The prod overlay
    hardcoded a small env block on bootstrap-substrate and didn't
    forward the SSH-CA role allowlists, so an operator extending
    SSH_CA_VAULT_ALLOWED_USERS to add a custom login account (e.g.
    justinfudally) saw their edit silently ignored — Compose's
    --env-file only loads values for YAML interpolation, not for
    container env. Vault then kept refusing to sign user certs with
    the new principal at client provision time
    (<user> is not a valid value for valid_principals).
    Adds the passthrough in docker-compose.prod.yml (mirroring the
    existing DEFAULT_SUBNET shape), the example entry in
    .env.prod.example, and a regression test in
    tests/test_compose_prod_bootstrap.py so a future env-block
    cleanup can't quietly regress this.

Changed

  • Register-server flow can now bootstrap the host in the same
    task.
    Replaces the standalone "Bootstrap host" dashboard action
    and POST /bootstrap-host endpoint that shipped in v0.3.x with
    a collapsible "Bootstrap this host first" section inside the
    Register-server form. When the operator pastes their OOB SSH
    private key there, provision_server_task opens one
    BootstrapSSHRunner session (TOFU + operator key) before the
    regular CA-mode provision session, laying down the SSH CA trust

    • signed host cert + sshd drop-in. One row, one task, one click
      for fresh boxes; same behaviour as today when the box was
      already bootstrapped (CLI path, baked AMI).
    • ServerCreate schema (src/wg_manager/schemas.py) gains
      optional bootstrap_ssh_key_pem + bootstrap_ssh_key_passphrase
      fields. POST /servers encrypts both via the crypto backend
      (Vault Transit in prod) before queueing and forwards the
      ciphertext to the task. Passphrase without PEM is rejected at
      the schema layer so the operator's intent isn't silently
      dropped.
    • provision_server_task (src/wg_manager/tasks.py) accepts
      the new encrypted-bootstrap kwargs, decrypts in worker memory,
      and runs bootstrap_host() before opening the CA-mode session.
      The bootstrap step is skipped entirely when no PEM was supplied;
      today's "you forgot to bootstrap" failure mode
      (host cert signed by an untrusted CA) is preserved verbatim.
    • Dashboard /servers (web/app/servers/page.tsx) drops the
      standalone "Bootstrap host" button; the bootstrap section now
      lives inside the Register form behind a <details> toggle,
      defaulting collapsed so the common (already-bootstrapped) case
      stays one click.
    • Removed: POST /bootstrap-host, bootstrap_host_task,
      BootstrapHostRequest / BootstrapHostResponse schemas,
      api.bootstrapHost() client method, and the matching tests.
      The CLI (wg-manager bootstrap-host) still exists for
      scripted/CI use that wants the install separate from
      registration.
    • Docs: docs/deploy/single-host-prod.md updates "Path A —
      Dashboard" to describe the combined Register-with-bootstrap
      flow; Path B (CLI) is unchanged.

Published images

  • ghcr.io/jfudally/wg_manager:v0.4.0 — API + worker
  • ghcr.io/jfudally/wg_manager-web:v0.4.0 — dashboard

Pull either with a granularity-appropriate tag (vX.Y.Z, vX.Y, vX, or latest).

Supply-chain attestation

Both images are signed via cosign keyless OIDC and carry an
in-toto CycloneDX SBOM attestation. Verify with:

cosign verify \
    --certificate-identity-regexp \
        'https://github.com/jfudally/wg_manager/.github/workflows/release.yml@.*' \
    --certificate-oidc-issuer \
        'https://token.actions.githubusercontent.com' \
    ghcr.io/jfudally/wg_manager:v0.4.0

The SBOMs are also attached as release assets:

  • sbom-api.cdx.json — CycloneDX SBOM of the API + worker image's Python deps
  • sbom-web.cdx.json — CycloneDX SBOM of the dashboard image's Node deps

v0.3.0

Choose a tag to compare

@github-actions github-actions released this 05 Jun 21:30
dfd6da3

Release theme: Vault production posture. Closes the largest
"Known limitations" entry in single-host-prod.md — Vault now
runs with a real file storage backend instead of -dev mode,
and an automated init+unseal layer keeps the operator UX identical
to v0.2.x (edit .env.prod, make prod-up, done). State persists
across container restarts, so the bootstrap-host-installed CA
pubkey on a target host stays valid through a Vault container
restart — the exact bug class that motivated this work.

Added

  • Production-mode Vault with auto-init + auto-unseal (PR #54).
    The prod overlay swaps vault server -dev (in-memory, fixed
    root token) for vault server -config=/vault/config/vault.hcl
    with file storage at /vault/file on the
    wg_manager_vault_data named volume.

    • docker/vault/vault.hcl — file storage, HTTP listener on
      0.0.0.0:8200, ui on, disable_mlock = true for portability.
      No listener TLS — Vault is the PKI source for every other
      cert in the stack, so the listener can't depend on a Vault-
      minted cert at boot. A dedicated listener-TLS cycle is the
      honest next step.
    • scripts/vault_init_unseal.sh — idempotent state machine
      that runs from prod_bootstrap_substrate.sh BEFORE the engine
      bootstraps. Probes /v1/sys/init + /v1/sys/seal-status. On
      uninit: calls vault operator init via the HTTP API with 5
      shares / 3 threshold, captures the JSON output to
      ${VAULT_INIT_FILE} (default /app/vault-init.json) at mode
      0600 owned by UID 1001 (the wg-manager runtime UID), then
      unseals. On sealed: reads the same file and unseals. On
      unsealed: no-op.
    • docker/entrypoint-wg-manager.sh — tiny shim baked into
      the wg-manager image. Reads vault-init.json when present +
      non-empty, exports the root_token field as VAULT_TOKEN
      before exec-ing the CMD. First-boot tolerant (empty file
      → no-op).
    • docker-compose.prod.yml vault service drops VAULT_DEV_*
      • VAULT_TOKEN env (blanked to "" so Compose's environment-
        map merge doesn't ride the dev compose's values through).
        entrypoint: ["vault"] bypasses the official image's
        docker-entrypoint.sh which would otherwise APPEND
        -dev-listen-address and race vault.hcl's listener.
        Healthcheck override accepts 501 (uninit) / 503 (sealed) as
        healthy via ?standbyok=true&sealedcode=204&uninitcode=204
        so bootstrap-substrate's depends_on: { vault: service_healthy } trips on the listener being up
        rather than the substrate being bootstrapped.
    • api / worker / bootstrap-app lose their VAULT_TOKEN
      env — the entrypoint shim sources it from vault-init.json
      instead. All four containers bind-mount vault-init.json
      (substrate writable, the rest read-only).
    • .env.prod.example drops VAULT_ROOT_TOKEN (auto-
      generated now), adds optional VAULT_KEY_SHARES /
      VAULT_KEY_THRESHOLD (defaults 5 / 3), documents the backup
      story (back up vault-init.json alongside .env.prod).
    • single-host-prod.md Known Limitations table flips the
      "Vault in dev mode" row to "Vault unseal keys live on disk
      in vault-init.json" — the honest residual gap. Real
      cloud-KMS auto-unseal (transit / awskms / gcpckms) is the
      next step but out of scope without cloud creds. The "Where
      state lives" table grows the three-file backup story
      (.env.prod + vault-init.json + tls/).
    • End-to-end verified on rv.vpn: clean make prod-up from
      zero state brings all 7 services healthy. Smoke on
      /healthz, /readyz, /tenants (default-tenant
      subnet_pool reflects the operator's DEFAULT_SUBNET),
      /crypto/status (Vault Transit backend) all 200. Critical
      restart test
      : docker restart wg_manager_vault → sealed →
      re-run bootstrap-substrate → auto-unseals from existing
      vault-init.json → all engine bootstraps log "already
      present" → Phase 1 complete. Final status:
      storage_type: file, initialized: true, sealed: false.

    Five bugs surfaced + fixed during verification (the full diary
    is in PR #54's body): vault image's docker-entrypoint.sh
    mangling the server command, missing urllib.error import,
    unquoted heredoc backtick command-substitution,
    vault-init.json UID mismatch from the Makefile touch, and a
    duplicate stale wait loop in prod_bootstrap_substrate.sh.

    Tests: 47 new cases across three new files plus extensions
    to four existing files. Backend pytest 1027/1027 in local
    mode (was 989 on v0.2.x's merge).

  • docs/deploy/single-host-prod.md gained an "Onboarding a
    target host (SSH CA install)" section (PR #53).
    Recipe for
    running wg-manager bootstrap-host from inside the prod stack
    via docker compose run --rm, mounting ~/.ssh read-only so
    the operator's pre-existing key reaches the container without
    exposing the rest of $HOME. The full design + cert profiles
    still live in docs/operator-guide.md §3; this is a
    deployment-side recipe rather than a duplicate. Closes the gap
    a real operator hit on rv.vpn: after make prod-up returned
    and they curl'd the API successfully, the next step ("how do I
    get wg-manager onto my first hub box?") wasn't covered in the
    deploy doc.


Published images

  • ghcr.io/jfudally/wg_manager:v0.3.0 — API + worker
  • ghcr.io/jfudally/wg_manager-web:v0.3.0 — dashboard

Pull either with a granularity-appropriate tag (vX.Y.Z, vX.Y, vX, or latest).

Supply-chain attestation

Both images are signed via cosign keyless OIDC and carry an
in-toto CycloneDX SBOM attestation. Verify with:

cosign verify \
    --certificate-identity-regexp \
        'https://github.com/jfudally/wg_manager/.github/workflows/release.yml@.*' \
    --certificate-oidc-issuer \
        'https://token.actions.githubusercontent.com' \
    ghcr.io/jfudally/wg_manager:v0.3.0

The SBOMs are also attached as release assets:

  • sbom-api.cdx.json — CycloneDX SBOM of the API + worker image's Python deps
  • sbom-web.cdx.json — CycloneDX SBOM of the dashboard image's Node deps

v0.2.1

Choose a tag to compare

@github-actions github-actions released this 05 Jun 16:22
da71d11

Patch release: closes a single bug class the v0.2.0 verification on
rv.vpn surfaced — DEFAULT_SUBNET (and DEFAULT_WG_PORT) set in
.env.prod were silently ignored by the production stack. The v0.2.0
prod overlay never propagated those ${VAR}s into the containers
that needed them; operators set the override, watched nothing
change, and saw the default tenant come up with the code-default
10.9.0.0/24 regardless.

Fixed

  • DEFAULT_SUBNET + DEFAULT_WG_PORT now flow from .env.prod
    through the prod overlay (PR #50 + PR #51).
    The two PRs together
    cover the full data path: request-time + migration-time.

    • PR #50 — pass the two env vars through to api + worker.
      Without this, Settings.default_subnet on the running api falls
      back to the code default (config.py:60 = 10.9.0.0/24) on
      every POST /servers payload that omits the subnet field. The
      dashboard's "New server" form pre-fills from this value too, so
      an operator who set the override in .env.prod would see the
      pre-fill ignore them.
    • PR #51 — same passthrough for bootstrap-app. Alembic
      migration 0016_add_tenant_subnet_pool.py:68 reads
      live_settings.default_subnet AT MIGRATION TIME to backfill the
      default tenant's subnet_pool. The migration runs inside
      bootstrap-app (the container that does alembic upgrade head),
      NOT inside api/worker. So even after PR #50 fixed the request-
      time path, the default tenant's subnet_pool still came up at
      10.9.0.0/24 on every fresh-volume boot because bootstrap-app
      was missing the env. PR #51 closes the loop.

    Verified end-to-end on rv.vpn after both PRs landed: with
    DEFAULT_SUBNET=10.8.0.0/24 in .env.prod, make prod-down -v +
    make prod-up brings the default tenant up with
    subnet_pool='10.8.0.0/24' (confirmed via both
    SELECT * FROM tenant directly against MySQL and
    GET /v1/tenants through the mTLS API).

    Operator remediation on a stack already bootstrapped with the
    buggy v0.2.0 default:

    • A. make prod-down -v + make prod-up on v0.2.1 — re-runs
      migration 0016 against the env-corrected bootstrap-app. Wipes
      state.
    • B. Manual SQL — UPDATE tenant SET subnet_pool='<your-cidr>' WHERE id=1. Preserves state.

    .env.prod.example grew a documented DEFAULT_SUBNET +
    DEFAULT_WG_PORT block explaining when an operator overrides
    (overlapping VPN address space with another stack, conflicting
    host routes). Tests grew two parametrised cases pinning the
    passthrough on api + bootstrap-app so the bug class can't
    re-emerge silently.


Published images

  • ghcr.io/jfudally/wg_manager:v0.2.1 — API + worker
  • ghcr.io/jfudally/wg_manager-web:v0.2.1 — dashboard

Pull either with a granularity-appropriate tag (vX.Y.Z, vX.Y, vX, or latest).

Supply-chain attestation

Both images are signed via cosign keyless OIDC and carry an
in-toto CycloneDX SBOM attestation. Verify with:

cosign verify \
    --certificate-identity-regexp \
        'https://github.com/jfudally/wg_manager/.github/workflows/release.yml@.*' \
    --certificate-oidc-issuer \
        'https://token.actions.githubusercontent.com' \
    ghcr.io/jfudally/wg_manager:v0.2.1

The SBOMs are also attached as release assets:

  • sbom-api.cdx.json — CycloneDX SBOM of the API + worker image's Python deps
  • sbom-web.cdx.json — CycloneDX SBOM of the dashboard image's Node deps

v0.2.0

Choose a tag to compare

@github-actions github-actions released this 05 Jun 15:17
03df4a1

Release theme: production deployability. The dev compose became
an actual production compose (overlay + self-bootstrap), the HA
control plane went from doc to demo profile, and the observability

  • multi-tenancy + API-versioning work that landed since v0.1.0 came
    along for the ride. Every shipped path picked up a regression test
    the end-to-end verification work surfaced.

make prod-up is the single command from a fresh checkout to a
fully usable production stack: edit five values in .env.prod,
wait ~90 seconds, get an mTLS API + Celery worker + Next.js
dashboard + hardened MySQL + Vault + Valkey on the box.

Added

  • Vault Transit engine + master key auto-bootstrap. Closes the
    last gap in make prod-up: scripts/transit_bootstrap.py is
    invoked by scripts/prod_bootstrap_substrate.sh so a fresh
    Vault gets the Transit mount + the wg-manager master key
    (derived=True) provisioned automatically. Before this, the
    dashboard's Crypto page 500'd with
    no handler for route transit/keys/wg-manager on first load
    because VaultTransitBackend is deliberately dumb and the
    cookbook documented Transit setup as a manual step. Also exposed
    as make transit-bootstrap for operators driving Vault by hand.

  • Linux portability fixes for the self-bootstrap (PR #47). PR
    #46's self-bootstrap worked on Docker Desktop on Mac but failed
    on every step against a real Linux host. Four distinct fixes:

    • Bootstrap containers declare user: "0:0" so they can
      write to the bind-mounted ./tls on Linux hosts where the
      operator's UID (typically 1000) doesn't match the wg-manager
      image's wgmanager UID (1001). Docker Desktop on Mac papers
      over the mismatch transparently — pure Linux doesn't. End-of-
      script chown -R 1001:1001 puts the cert files back into the
      runtime UID so api/worker/web can read them.
    • *Bootstrap scripts chmod .key files to 0644 so mysql:8
      inside its container (UID 999) can read the server.key even
      though the file is owned by UID 1001. Without this, mysqld
      fails to load TLS and the app gets
      SSL is required but the server doesn't support it on every
      connection attempt.
    • WG_MANAGER_API_BIND_PORT + WG_MANAGER_WEB_BIND_PORT
      env overrides for operators running multiple stacks on one
      host (or fronting the API behind a reverse proxy that owns
      443).
    • .env.prod.example now recommends openssl rand -hex 32
      for VALKEY_PASSWORD + MYSQL_APP_PASSWORD — these values
      get interpolated into redis://:${pw}@valkey:... and
      mysql+pymysql://wg:${pw}@mysql:... URLs, and the previous
      openssl rand -base64 recommendation produced strings with
      /+= characters that broke URL parsing at startup.
  • Self-bootstrapping docker-compose.prod.yml. Edit .env.prod,
    run make prod-up, get a fully usable production-shaped stack on
    the first invocation — no manual cert minting, no shell exports,
    no separate make migrate step. The previous 9-step runbook
    collapses to "fill in 5 values, run one command".

    • Two run-to-completion bootstrap containers break the
      cert/MySQL chicken-and-egg cleanly via Compose's
      service_completed_successfully primitive:
      • bootstrap-substrate — waits for Vault healthy, runs the
        Vault PKI / SSH CA / audit bootstraps (each idempotent), mints
        the MySQL server + client cert pair (--type mysql +
        --type mysql-client for the correct EKU split), exits.
        mysql + valkey depend on it completing successfully.
      • bootstrap-app — waits for MySQL + valkey healthy, runs
        alembic upgrade head, registers the bootstrap operator,
        mints the API server cert + operator CLI client cert, exits.
        api + worker + web depend on it completing.
    • scripts/prod_bootstrap_substrate.sh +
      scripts/prod_bootstrap_app.sh are the two scripts the
      bootstrap containers exec. Both set -euo pipefail and guard
      every mutating step with a file-existence test, so re-running
      make prod-up against persistent state is a no-op (cert files
      present → skip mint; operator registered → skip; alembic at
      head → no-op).
    • alembic moved from dev to runtime deps so the wg-manager
      image bakes /app/.venv/bin/alembic — the bootstrap container
      needs it, and operators pulling
      ghcr.io/jfudally/wg-manager:vX.Y.Z shouldn't have to install
      dev deps to apply migrations.
    • make prod-up gains --wait so the command blocks until
      every service is healthy or exited 0. When the make target
      returns, the stack is fully usable end-to-end.
    • .env.prod.example now documents the operator identity
      (BOOTSTRAP_OPERATOR_CN required, BOOTSTRAP_OPERATOR_ROLE
      optional with default admin) and the API server cert subject
      (API_SERVER_CN + API_SERVER_SANS, both optional with
      localhost-friendly defaults). The old comment block that left
      BOOTSTRAP_OPERATOR_CN commented-out is gone — the prod stack
      requires it.
    • Runbook rewrite at docs/deploy/single-host-prod.md: the
      "Bootstrap (one command)" section is the canonical flow; the
      old 9-step manual flow is preserved as an "Advanced: manual
      bootstrap (for debugging)" section operators can fall back to
      when self-bootstrap fails partway and they want shell-level
      introspection.
    • End-to-end verification on a clean Docker host:
      make prod-down -v → strip tls/make prod-up. Sequence:
      bootstrap-substrate exited → mysql + valkey healthy →
      bootstrap-app exited → api + worker + web healthy. Curl on
      /v1/healthz + /v1/readyz returns 200, /v1/servers
      authenticates via the operator cert and returns [], dashboard
      returns 200. Re-running make prod-up is a clean no-op (all
      bootstrap steps log "already exists, skipping").
    • Tests: 16 compose-shape cases
      (tests/test_compose_prod_bootstrap.py) + 18 script-shape
      cases (tests/test_prod_bootstrap_scripts.py) = 34 new. The compose
      suite pins the dependency graph (the bootstrap ordering is
      encoded entirely in depends_on conditions, so a future edit
      that breaks the graph trips at test time). The scripts suite
      pins shebangs, set -euo pipefail, the substrate script's
      invocation of all three Vault bootstrap helpers, the app
      script's alembic upgrade head + operators add + the two
      cert mints, and the file-existence-guard pattern.
  • Phase 3d cycle 4a — docker-compose ha profile + nginx
    passthrough LB.
    Materialises the two-replica + LB topology from
    docs/deploy/ha-control-plane.md on a single host so an operator
    can verify failover end-to-end before deploying a real two-host
    setup.

    • Compose ha profile. Three new services in
      docker-compose.yml, all gated behind profiles: ["ha"]:
      api1 (host port 8001), api2 (host port 8002), and lb
      (host port 8443, nginx:1.27-alpine). Both replicas build the
      existing Phase 2f Dockerfile, bind-mount the dev cert bundle
      in tls/ read-only, and reuse the default-profile data tier
      (mysql + valkey + vault + vector — unprofiled so it comes up
      under both flows).
    • nginx LB config at docker/nginx/wg-manager.conf is
      stream {}-mode TCP passthrough — the HA topology forbids TLS
      termination at the LB so the mTLS handshake lands on the
      replica intact. Two upstreams (api1:8000, api2:8000) with
      passive max_fails=3 fail_timeout=10s checks, single listener
      on 8443.
    • Makefile grew ha-up / ha-down / ha-logs mirroring the
      db-up family.
    • Docs. docs/deploy/ha-control-plane.md gained a "Running
      the ha profile locally" section with the three-endpoint table,
      a failover-smoke recipe, and the explicit list of what cycle
      4a does not ship (active /readyz probing at the LB, TLS
      termination at the LB, MySQL primary→replica plumbing).
    • Tests: 19 compose-shape cases
      (tests/test_compose_ha_profile.py) + 7 nginx-config cases
      (tests/test_nginx_lb_config.py) — pure YAML/text parse-and-
      assert, matches the Phase 2f test_dockerfile.py pattern. CI's
      image-build workflow remains the live compose validator; these
      tests pin the source-of-truth shape so a refactor that drops a
      service or breaks the passthrough contract trips before merge.
  • End-to-end verification of the prod overlay surfaced four bugs;
    all four fixed.
    Walking the runbook on a clean Docker host
    exercised every shipped code path the overlay touches and turned
    up code-level shape mismatches between what the docs promised and
    what the code delivered. Each is fixed with a regression test.

    • Compose ports: list merge — the overlay's
      127.0.0.1:3306:3306 mappings were being concatenated with the
      dev file's 0.0.0.0:3307:3306 instead of replacing it, so the
      second host-port bind failed silently. Fixed with Compose v2.20's
      !override tag on mysql / valkey / vault ports. Test
      fixtures grew a custom ComposeLoader so PyYAML tolerates the
      !override / !reset Compose-specific tags.
    • PEM chain newline missing. VaultPKI._issue_leaf joined the
      Vault ca_chain list with "".join(...) and LocalDevPKI
      used intermediate_pem + root_pem. When the upstream PEM
      lacked a trailing newline (Vault's modern API output), the
      result was -----END CERTIFICATE----------BEGIN CERTIFICATE-----
      on one line — rejected by strict consumers (openssl x509,
      Python ssl.SSLContext.load_verify_locations, pymysql's
      ssl={ca: ...}). New wg_manager.pki._join_pems(*pems) helper
      normalises to strict-parser-safe output and is used at both
      sites. 10 regression cases in tests/test_pki_chain_join.py.
    • Alembic env.py ignored MySQL TLS args.
      engine_from_config(config_section, prefix="sqlalchemy.") built
      its engine wi...
Read more

v0.1.0

Choose a tag to compare

@github-actions github-actions released this 03 Jun 19:31
045d2bb

Added

  • Phase 2f cycle 4 — SBOM generation + attestation + release-asset
    attachment.
    Closes the Phase 2e SBOM bullet and the final
    Phase 2f cycle. Phase 2 is closed. Every published release now
    ships with two CycloneDX 1.5 JSON SBOMs covering the runtime dep
    closure of each image, delivered two ways for the verify path the
    consumer prefers.

    • The release workflow's API job runs cyclonedx-py environment
      against the synced .venv (uv sync --frozen --no-dev matches
      the production image content) and emits sbom-api.cdx.json.
      The web job runs @cyclonedx/cyclonedx-npm --omit dev against
      web/node_modules after npm ci and emits sbom-web.cdx.json.
    • In-toto attestation on the image: cosign attest --yes --type cyclonedx --predicate sbom-*.cdx.json "${IMAGE}@${DIGEST}" binds each SBOM to the immutable image
      digest. Same Fulcio identity as the cycle 3 signature, so a
      future cosign verify-attestation gate proves SBOM provenance
      from the canonical workflow path.
    • Release asset: both SBOMs upload as workflow artifacts and
      the release job downloads + attaches them via
      gh release create … sbom-*.cdx.json. The release page now
      leads with a "Supply-chain attestation" section covering the
      canonical cosign verify invocation + SBOM filenames.
    • docs/release.md grows a "Software Bill of Materials" section
      covering both delivery paths, the cosign verify-attestation --type cyclonedx flow, and a jq-based recipe for diffing
      SBOMs between two releases.
    • 7 new test cases extending
      tests/test_release_workflow.py's TestSbomGeneration:
      cyclonedx-py + cyclonedx-npm referenced, cosign attest step
      with --type cyclonedx predicate, SBOMs uploaded via
      actions/upload-artifact, release job uses
      actions/download-artifact, gh release create lists the
      .cdx.json files as positional asset arguments.
  • Phase 2f cycle 3 — cosign keyless signing + verify gate.
    Closes Phase 2e's "Deferred — cosign verify" bullet. Every image
    the release workflow publishes is now signed with cosign keyless
    OIDC against GitHub Actions' Fulcio issuer; a separate verify
    workflow proves the signatures verify against the canonical
    identity.

    • .github/workflows/release.yml extended: each
      build-and-push-* job installs sigstore/cosign-installer@v3
      and runs cosign sign --yes "${IMAGE}@${DIGEST}" against the
      immutable digest the push step emits. Signing the digest (not
      the tag) means a future re-tag inherits the signature.
    • New .github/workflows/image-verify.yml is the consumer-side
      gate. Runs on workflow_dispatch (with a tag input,
      defaults to latest) and on a daily 14:00 UTC cron. Not
      on push/PR — verification before the first release is cut
      would always fail. Two jobs (verify-api + verify-web) run
      cosign verify with --certificate-identity-regexp pinned to
      the canonical release workflow path in this repo (catches
      fork-workflow / stolen-token / malicious-mirror signatures)
      and --certificate-oidc-issuer pinned to GitHub Actions'
      Fulcio issuer (catches Fulcio certs from other OIDC
      providers).
    • The cron failure mode is exactly the alerting trigger an
      operator wants: a previously-verified image no longer verifies
      → someone tampered with it after publish.
    • docs/release.md grows a "Verifying a published image"
      section covering the downstream-pull flow + the CI workflow
      path + what the identity binding catches.
    • 16 new test cases:
      tests/test_image_verify_workflow.py
      × 11 (workflow exists, triggers on dispatch + schedule but
      NOT push/PR, dispatch takes tag input, contents-read
      least-privilege perms, installs cosign, calls cosign verify,
      pins identity regexp + OIDC issuer, covers both images); plus
      5 cases extending tests/test_release_workflow.py's
      TestCosignSigning (installs cosign, signs pushed images,
      --yes flag, signs by digest not tag).
  • Phase 2f cycle 2 — tagged release workflow + GHCR publish.
    Closes the second of four Phase 2f cycles. A git push origin v<X.Y.Z> now produces published images at
    ghcr.io/<owner>/wg-manager:v<X.Y.Z> (and -web) plus a GitHub
    release with notes extracted from CHANGELOG.md's matching
    ## [v<X.Y.Z>] section.

    • New .github/workflows/release.yml with four jobs:
      extract-notes (parses CHANGELOG via the new helper), two
      parallel build-and-push jobs for the API + web Dockerfiles,
      and a release job that creates the GitHub release via
      gh release create --verify-tag. docker/metadata-action
      derives semver / SHA / latest tags from the git ref.
      Permissions: contents: write + packages: write +
      id-token: write (cycle 3 layers cosign keyless signing on
      the same workflow). Concurrency is not
      cancel-in-progress — partial GHCR pushes are messier than
      a stuck job.
    • New scripts/extract_changelog.py walks CHANGELOG.md and
      returns the body of the ## [vX.Y.Z] section matching the tag
      being released. Fails the workflow loudly when the heading is
      missing — operators promote ## [Unreleased] to the versioned
      heading before tagging.
    • New make release-notes VERSION=vX.Y.Z wraps the extractor
      for local preview.
    • New docs/release.md operator runbook walks the
      promote-Unreleased → tag → workflow flow and two recovery
      paths for a mid-flight failure.
    • 24 new test cases:
      tests/test_extract_changelog.py
      × 8 (section matching, version-prefix normalisation,
      missing-version exit, CLI happy + error paths);
      tests/test_release_workflow.py
      × 16 (workflow shape, triggers, permissions, GHCR target,
      metadata-action, push: true, both Dockerfiles, extractor
      shell-out, release creation, no cancel-in-progress).
  • Phase 2f cycle 1 — Dockerfiles + image-build CI gate. Opens
    the release-engineering work-stream that Phase 2e deferred (signed
    Docker image publish, cosign verify, SBOM attachment). Cycle 1
    ships the foundation; cycles 2-4 publish, sign, and SBOM on top.

    • Multi-stage Dockerfile — Python 3.13-slim-
      bookworm builder runs uv sync --frozen --no-dev against the
      locked deps, slim runtime carries only .venv + src/ and
      drops to non-root wgmanager user UID 1001.
      python -m wg_manager is the default CMD.
    • Multi-stage web/Dockerfile — Node 22-slim
      builder runs npm ci + npm run build, runtime copies the
      .next/standalone bundle and runs as non-root nextjs UID
      1001. web/next.config.ts flipped to output: "standalone"
      so the standalone copy has something to copy.
    • New .github/workflows/image-build.yml
      builds both images on every PR + push to main via
      docker/build-push-action@v6 with GHA layer cache. push: false pinned so cycle 1 explicitly does not publish (cycle 2
      territory). Path-filtered to dep manifests + source dirs so
      code-only PRs skip the ~3-min image build.
    • Latent build bugs surfaced + fixed: the tailwindcss v3→v4
      dependabot bump left next dev working but next build
      broken. (a) PostCSS plugin moved to @tailwindcss/postcss
      web/postcss.config.mjs updated.
      (b) v3 @tailwind base/components/utilities directives
      dropped — web/app/globals.css
      migrated to @import "tailwindcss" + @theme block with
      --color-* tokens replacing the tailwind.config.ts
      theme.extend.colors block. (c) Pre-existing
      web/lib/proxy.ts:124 Uint8Array<ArrayBufferLike>
      BodyInit complaint (flagged out-of-scope in 2d CP3.4)
      blocks next build — patched with the documented type
      workaround. None of this regressed before cycle 1 because the
      existing CI runs vitest only (not next build).
    • 26 new test cases: tests/test_dockerfile.py
      × 18 (Dockerfile existence, multi-stage shape, slim base,
      Python 3.13 pin, uv --frozen, non-root user, WORKDIR,
      default CMD; same shape for web/Dockerfile + standalone-output
      check on next.config.ts); tests/test_image_build_workflow.py
      × 8 (workflow exists with descriptive name, triggers on PR +
      push to main, path-filtered, uses docker/build-push-action,
      references both Dockerfiles by path, pins push: false,
      cancels-in-progress concurrency, contents-read least-privilege
      permissions). Pure parse-and-assert — the live docker build
      is what the CI workflow runs.

    Backend pytest 632/632 green (was 606 on Phase 2e cycle 4).
    Vitest 46/46. Local smoke: both docker build invocations
    succeed.

  • Phase 2e cycle 4 — wg-manager evidence pack SOC 2 evidence
    tarball.
    Closes the ROADMAP Phase 2e stretch acceptance bullet.
    New wg-manager evidence pack --output PATH --since-days N --vault-audit-log PATH CLI command + wg_manager.evidence
    module assemble a tar.gz an auditor can verify end-to-end.

    • Pack contents: audit_events.json (auditevent table filtered
      to last N days), certificates.json + operators.json (full
      registry dumps — current state, no date filter), vault_audit.log
      (Vault audit file sliced to the same window by parsing each
      line's time field), vault_audit_integrity.json (per-line
      JSON parseability + time field presence + request/response
      request.id pairing — honest about being structural only
      because Vault does not ship a cryptographic chain across audit
      records), system.json (wg-manager version, git commit,
      alembic head), `M...
Read more