Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
20 changes: 13 additions & 7 deletions .github/scripts/notify-publish-safety.rb
Original file line number Diff line number Diff line change
Expand Up @@ -544,6 +544,12 @@ def resume_action(existing_digest, expected_digest, mutable:)
"image selector must require both mutually exclusive image paths")
assert_policy(jobs.fetch("attest-binaries").fetch("needs").include?("image-ready"),
"normal binary attestation must follow immutable image selection")
attest_binaries_condition = jobs.fetch("attest-binaries").fetch("if")
assert_policy(attest_binaries_condition.include?("always()") &&
%w[publish-safety-policy notify-guard publish-gate image-ready].all? { |name|
attest_binaries_condition.include?("needs.#{name}.result == 'success'")
},
"normal binary attestation must survive the intentionally skipped recovery path")
assert_policy((%w[image-ready attest-binaries] -
jobs.fetch("binary-attestation-ready").fetch("needs")).empty?,
"binary attestation selector must require image selection and the tag-only attestation job")
Expand Down Expand Up @@ -616,10 +622,10 @@ def resume_action(existing_digest, expected_digest, mutable:)
"publish gate must fail closed and export the finalized public-release state")

assert_policy(spec.fetch("format_version") == 1, "release manifest format changed")
assert_policy(spec.fetch("version") == "2.0.1", "release version must remain 2.0.1")
assert_policy(spec.fetch("tag") == "notify-v2.0.1", "release tag must remain notify-v2.0.1")
assert_policy(spec.fetch("version") == "2.0.2", "release version must remain 2.0.2")
assert_policy(spec.fetch("tag") == "notify-v2.0.2", "release tag must remain notify-v2.0.2")
assert_policy(spec.fetch("image") == "ghcr.io/psimaker/vaultsync-notify", "image repository changed")
assert_policy(spec.fetch("version_image") == "ghcr.io/psimaker/vaultsync-notify:2.0.1",
assert_policy(spec.fetch("version_image") == "ghcr.io/psimaker/vaultsync-notify:2.0.2",
"version image changed")
assert_policy(spec.fetch("binaries") == EXPECTED_BINARIES, "expected binary set or order changed")
assert_policy(spec.fetch("release_assets").sort == EXPECTED_ASSETS.sort, "release asset set changed")
Expand All @@ -639,9 +645,9 @@ def resume_action(existing_digest, expected_digest, mutable:)

install_text = File.read(INSTALL_PATH)
compose_text = File.read(COMPOSE_PATH)
assert_policy(install_text.include?("ghcr.io/psimaker/vaultsync-notify:2.0.1"),
assert_policy(install_text.include?("ghcr.io/psimaker/vaultsync-notify:2.0.2"),
"installer default is not the reviewed version tag")
assert_policy(compose_text.include?("ghcr.io/psimaker/vaultsync-notify:2.0.1"),
assert_policy(compose_text.include?("ghcr.io/psimaker/vaultsync-notify:2.0.2"),
"Compose default is not the reviewed version tag")
assert_policy(!install_text.include?("ghcr.io/psimaker/vaultsync-notify:latest") &&
!compose_text.include?("ghcr.io/psimaker/vaultsync-notify:latest"),
Expand All @@ -663,7 +669,7 @@ def resume_action(existing_digest, expected_digest, mutable:)
end
assert_policy(!security_resolve_step.nil?, "scheduled scan release resolver is missing")
security_release_env = security_resolve_step.fetch("env", {})
assert_policy(security_release_env.fetch("TARGET_RELEASE_TAG", nil) == "notify-v2.0.1" &&
assert_policy(security_release_env.fetch("TARGET_RELEASE_TAG", nil) == "notify-v2.0.2" &&
security_release_env.fetch("FALLBACK_RELEASE_TAG", nil) == "notify-v2.0.0",
"scheduled scan must use the reviewed target and last public fallback releases")
assert_policy(security_text.include?("IMAGE-DIGESTS"), "scheduled scan must resolve the release digest")
Expand Down Expand Up @@ -708,7 +714,7 @@ def resume_action(existing_digest, expected_digest, mutable:)
base.merge(recovery_run_id: "29324314809"),
base.merge(ref_type: "branch", ref_name: "main", recovery_run_id: "0"),
base.merge(ref_type: "branch", ref_name: "main", recovery_run_id: "failed-run"),
base.merge(release_tag: "notify-v2.0.0"),
base.merge(release_tag: "notify-v2.0.1"),
base.merge(confirmation: "publish"),
base.merge(actor: "maintainer"),
base.merge(triggering_actor: "maintainer"),
Expand Down
7 changes: 6 additions & 1 deletion .github/workflows/docker.yml
Original file line number Diff line number Diff line change
Expand Up @@ -615,13 +615,18 @@ jobs:
attest-binaries:
name: Attest Release Binaries
if: >-
always() &&
github.event_name == 'workflow_dispatch' &&
github.ref_type == 'tag' &&
inputs.release_tag == github.ref_name &&
inputs.recovery_run_id == '' &&
inputs.confirmation == 'PUBLISH_NOTIFY_RELEASE' &&
github.actor == github.repository_owner &&
github.triggering_actor == github.repository_owner
github.triggering_actor == github.repository_owner &&
needs.publish-safety-policy.result == 'success' &&
needs.notify-guard.result == 'success' &&
needs.publish-gate.result == 'success' &&
needs.image-ready.result == 'success'
needs:
- publish-safety-policy
- notify-guard
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/security.yml
Original file line number Diff line number Diff line change
Expand Up @@ -126,7 +126,7 @@ jobs:
id: release-image
env:
GH_TOKEN: ${{ github.token }}
TARGET_RELEASE_TAG: notify-v2.0.1
TARGET_RELEASE_TAG: notify-v2.0.2
FALLBACK_RELEASE_TAG: notify-v2.0.0
run: |
set -euo pipefail
Expand Down
54 changes: 31 additions & 23 deletions docs/helper-publication-rollout.md
Original file line number Diff line number Diff line change
@@ -1,10 +1,17 @@
# Helper 2.0.1 publication and helper-first rollout
# Helper 2.0.2 publication and helper-first rollout

This document defines the owner-gated publication, compatibility, rollback,
monitoring, and recovery contract for `vaultsync-notify` 2.0.1. This patch
supersedes 2.0.0 because that release did not expose D022's pending transcript
fingerprint to the local operator; no 2.0.0 artifact or tag is changed. It does not by
itself claim that publication ran. The GitHub release, its
monitoring, and recovery contract for `vaultsync-notify` 2.0.2. This patch
supersedes 2.0.1 after tag-bound workflow run `29358780832` published only the
scanned and attested image index
`sha256:2c0f620fa1ce934ae75b358a60935460d244b19d4e98718728dd0338e0b24229`.
GitHub Actions then propagated the intentionally skipped recovery path past the
normal binary-attestation job, so no binaries, GitHub release, helper-first
rollout, or public verification were produced. The 2.0.1 tag and image remain
unchanged and are not completion evidence. Version 2.0.1 had superseded 2.0.0
because 2.0.0 did not expose D022's pending transcript fingerprint to the local
operator; no 2.0.0 artifact or tag is changed. This document does not by itself
claim that 2.0.2 publication ran. The GitHub release, its
`RELEASE-MANIFEST.json`, the exact workflow run, and the registry digest are the
canonical post-publication evidence.

Expand All @@ -17,9 +24,9 @@ upload, download, or roundtrip product evidence follows from publication.
The reviewed source manifest is [`../notify/release.json`](../notify/release.json).
It fixes:

- version `2.0.1` and tag `notify-v2.0.1`;
- version `2.0.2` and tag `notify-v2.0.2`;
- image repository `ghcr.io/psimaker/vaultsync-notify`, with the only new tag
`2.0.1`;
`2.0.2`;
- five expected binaries for Linux amd64/arm64, macOS amd64/arm64, and Windows
amd64;
- the previous public rollback tag, commit, and multi-platform image digest;
Expand Down Expand Up @@ -53,13 +60,13 @@ repository security setting requires separate authorization.

| Artifact | Required proof |
|---|---|
| OCI index for Linux amd64 and arm64 | Exact index and platform SHA-256 digests; source/version labels; embedded `vaultsync-notify 2.0.1`; BuildKit provenance/SBOM plus GitHub repository-bound provenance and SPDX SBOM attestations. |
| OCI index for Linux amd64 and arm64 | Exact index and platform SHA-256 digests; source/version labels; embedded `vaultsync-notify 2.0.2`; BuildKit provenance/SBOM plus GitHub repository-bound provenance and SPDX SBOM attestations. |
| Five static binaries | Rebuilt twice byte-identically from the tag commit, listed in `SHA256SUMS`, scanned, and covered by GitHub provenance and SPDX SBOM attestations. |
| `SHA256SUMS` | Exact checksum for each binary; installers fail closed if this asset or a local SHA-256 implementation is unavailable. |
| `SBOM.spdx.json` | SPDX 2.3 inventory for the release binary set. |
| `IMAGE-DIGESTS` | Image repository, version tag, OCI index digest, and Linux amd64/arm64 manifest digests. |
| `RELEASE-MANIFEST.json` | Release version/tag, exact source commit, image digest, rollback baseline, binary sizes/digests, SBOM digest, and expected asset set. The exact workflow run is recorded separately in rollout evidence. |
| `ROLLOUT-EVIDENCE.txt` | Exact old/new references and the supported-host upgrade, rollback, and forward-recovery results, including the observed legacy 1.8.0 endpoint-log boundary and the 2.0.1 sensitive-log assertion. |
| `ROLLOUT-EVIDENCE.txt` | Exact old/new references and the supported-host upgrade, rollback, and forward-recovery results, including the observed legacy 1.8.0 endpoint-log boundary and the 2.0.2 sensitive-log assertion. |

The image runtime is a static scratch image built from a digest-pinned Go
builder. Its CA bundle is copied from that same pinned builder. There is no
Expand All @@ -81,8 +88,8 @@ Publication is staged in this order:
release without replacing any asset.
4. On a fresh GitHub-hosted standard Linux runner using rootful Docker, execute
the real explicit diagnostics installer against only digest references:
published 1.8.0 → candidate 2.0.1 → published 1.8.0 → the same candidate
2.0.1.
published 1.8.0 → candidate 2.0.2 → published 1.8.0 → the same candidate
2.0.2.
5. Require the old helper to keep diagnostics unavailable, the new helper to
expose the TLS listener, the rollback to preserve credential-state bytes,
and forward recovery to preserve both those bytes and the TLS SPKI pin. The
Expand All @@ -107,14 +114,15 @@ Diagnostics is additive and opt-in; Trigger v1 and Relay v1 remain unchanged.
| App | Helper | Relay | Result |
|---|---|---|---|
| Released old app | Published old helper 1.8.0 | Existing old Relay | Existing Trigger-v1 behavior only. |
| Released old app | Published helper 2.0.1, diagnostics unset | Existing old Relay | Byte-compatible Trigger-v1 behavior. No listener, credential, namespace, trust, mapping, or diagnostics artifact is created. |
| Released old app | Helper 2.0.1 explicitly configured | Existing old Relay | Helper capability exists locally, but the old app never calls it. Upload, download, and roundtrip remain unset. |
| Any app | Partial image-only helper 2.0.1 | Any Relay v1 | Aborted publication state: no GitHub release, binaries, helper-first rollout, or public verification. The immutable image is retained for audit but is not a supported helper release. |
| Released old app | Published helper 2.0.2, diagnostics unset | Existing old Relay | Byte-compatible Trigger-v1 behavior. No listener, credential, namespace, trust, mapping, or diagnostics artifact is created. |
| Released old app | Helper 2.0.2 explicitly configured | Existing old Relay | Helper capability exists locally, but the old app never calls it. Upload, download, and roundtrip remain unset. |
| Future capable app | Published old helper 1.8.0 | Any supported Relay v1 | Honest capability unavailable. The app must not pair, create, trust, or transfer. |
| Future capable app | Helper 2.0.1, unconfigured/unpaired/unauthorized | Any supported Relay v1 | Honest unavailable/unsupported response; no fallback evidence and no automatic action. |
| Future capable app | Helper 2.0.1, exact explicit pairing and namespace authorization | Existing old Relay | The local D022–D024 helper contract may be used only after the explicit local transcript comparison. Relay remains outside upload/download/roundtrip correlation. |
| Future capable app | Helper 2.0.1 | Future new Relay | Same local diagnostics contract; Relay version does not strengthen sync evidence. |
| Any app | Helper 2.0.1 → 1.8.0 | Any Relay v1 | Diagnostics becomes unavailable. Credentials, namespace content, mappings, backups, versions, conflicts, and tombstones are not deleted or rewritten. The rollback also restores 1.8.0's legacy configured-endpoint startup log fields; operators must apply their existing log-access controls. |
| Any app | Helper 2.0.1 → 1.8.0 → exact 2.0.1 digest | Any Relay v1 | Forward recovery revalidates preserved state and requires current exact credentials/authorization. No operation resumes automatically. |
| Future capable app | Helper 2.0.2, unconfigured/unpaired/unauthorized | Any supported Relay v1 | Honest unavailable/unsupported response; no fallback evidence and no automatic action. |
| Future capable app | Helper 2.0.2, exact explicit pairing and namespace authorization | Existing old Relay | The local D022–D024 helper contract may be used only after the explicit local transcript comparison. Relay remains outside upload/download/roundtrip correlation. |
| Future capable app | Helper 2.0.2 | Future new Relay | Same local diagnostics contract; Relay version does not strengthen sync evidence. |
| Any app | Helper 2.0.2 → 1.8.0 | Any Relay v1 | Diagnostics becomes unavailable. Credentials, namespace content, mappings, backups, versions, conflicts, and tombstones are not deleted or rewritten. The rollback also restores 1.8.0's legacy configured-endpoint startup log fields; operators must apply their existing log-access controls. |
| Any app | Helper 2.0.2 → 1.8.0 → exact 2.0.2 digest | Any Relay v1 | Forward recovery revalidates preserved state and requires current exact credentials/authorization. No operation resumes automatically. |

The five downloadable binaries do not expand diagnostics packaging support.
Docker Host-Bind on a standard Linux host with rootful Docker remains the only
Expand All @@ -133,7 +141,7 @@ Publication aborts before the GitHub release becomes public on any of:
- missing/extra release asset, binary non-reproducibility, checksum mismatch,
or image architecture/version/label mismatch;
- failure of upgrade, rollback, forward recovery, state-byte preservation,
TLS-pin preservation, mount constraints, 2.0.1 sensitive-log exclusion, or
TLS-pin preservation, mount constraints, 2.0.2 sensitive-log exclusion, or
exact installer use. The separately asserted legacy endpoint fields from the
immutable 1.8.0 rollback image are a documented rollback boundary, not
candidate evidence.
Expand All @@ -155,12 +163,12 @@ or user-vault progress.

Existing installs are not migrated, paired, restarted, or reconfigured by
publication. The historical `latest` image tag is not moved. A user or operator
must explicitly rerun the installer, select the reviewed `2.0.1` version tag,
must explicitly rerun the installer, select the reviewed `2.0.2` version tag,
or pull the documented digest. The installer re-resolves the version tag and
runs the resulting local content ID; a network failure does not silently reuse
a stale tag.

Without both diagnostics configuration paths, 2.0.1 behaves as the prior
Without both diagnostics configuration paths, 2.0.2 behaves as the prior
Trigger-v1 helper and creates no diagnostics state. With explicit configuration,
pairing, namespace enablement, and later app operations remain separate signed
actions. No helper publication discovers Syncthing, changes its configuration,
Expand All @@ -176,8 +184,8 @@ delivery, or global health.
Emergency rollback also returns to 1.8.0's existing startup logging of
configured endpoint values. It does not log the API key in this proof, but
endpoint values can still be private deployment metadata. Restrict old-helper
log access and forward-recover to the exact 2.0.1 digest when the abort cause is
cleared; 2.0.1's rollout gate rejects those configured values in candidate
log access and forward-recover to the exact 2.0.2 digest when the abort cause is
cleared; 2.0.2's rollout gate rejects those configured values in candidate
logs.

## Evidence boundary after helper rollout
Expand Down
2 changes: 1 addition & 1 deletion docs/relay-spec.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# VaultSync Cloud Relay — Specification

> **Status:** Cloud Relay 1.3.0 and the matching app support are implemented and locally verified, but the Relay production rollout and VaultSync 2.0 app release are still pending. Once deployed, Relay 1.3 provisioning requires a verified StoreKit signed transaction and gives exact pre-existing legacy registrations a bounded compatibility window through October 31, 2026. Helper publication state is determined only by the newest public `notify-v*` release and its exact manifest; `notify-v1.8.0` remains the fixed rollback baseline for helper 2.0.1. Even a published capable helper provides no app upload, download, or correlated-roundtrip evidence until the separate app milestones succeed. A Relay-observed signal proves only accepted Relay processing: not helper identity, APNs delivery, background start, local data progress, upload, download, or a roundtrip. Existing Relay v1 provisioning, trigger, and push contracts remain unchanged. This document is the protocol and architecture reference for the relay, the `vaultsync-notify` sidecar, and the iOS client.
> **Status:** Cloud Relay 1.3.0 and the matching app support are implemented and locally verified, but the Relay production rollout and VaultSync 2.0 app release are still pending. Once deployed, Relay 1.3 provisioning requires a verified StoreKit signed transaction and gives exact pre-existing legacy registrations a bounded compatibility window through October 31, 2026. Helper publication state is determined only by the newest public `notify-v*` release and its exact manifest; `notify-v1.8.0` remains the fixed rollback baseline for helper 2.0.2. Even a published capable helper provides no app upload, download, or correlated-roundtrip evidence until the separate app milestones succeed. A Relay-observed signal proves only accepted Relay processing: not helper identity, APNs delivery, background start, local data progress, upload, download, or a roundtrip. Existing Relay v1 provisioning, trigger, and push contracts remain unchanged. This document is the protocol and architecture reference for the relay, the `vaultsync-notify` sidecar, and the iOS client.

## Overview

Expand Down
4 changes: 2 additions & 2 deletions notify/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,7 @@ docker run -d --name vaultsync-notify --restart unless-stopped \
-v /PATH/TO/syncthing:/config:ro \
-e SYNCTHING_CONFIG=/config/config.xml \
-e RELAY_URL=https://relay.vaultsync.eu \
ghcr.io/psimaker/vaultsync-notify:2.0.1
ghcr.io/psimaker/vaultsync-notify:2.0.2
```

Replace `/PATH/TO/syncthing` with your Syncthing config folder (often `~/.local/state/syncthing` or `~/.config/syncthing`). Permission error? Add `-u <uid>:<gid>` for the user that owns `config.xml`.
Expand Down Expand Up @@ -224,7 +224,7 @@ Validates: Syncthing API reachable · API key valid · Device ID readable · rel

**Runtime healthcheck** — the image's `HEALTHCHECK` runs `vaultsync-notify --healthcheck`, validating real readiness (Syncthing API, credentials, Device ID, relay health), not just process liveness. Peer state is deliberately excluded here: a legitimately offline peer must never flip the container to unhealthy.

**Version** — `vaultsync-notify --version` prints the installed helper version (needs no configuration). The installer shows old → new on upgrades. Docker installs pull the reviewed `2.0.1` tag and run only its resolved local content ID; failed pulls do not fall back to a stale tag. Binary installs select the newest published `notify-v*` release, require its `SHA256SUMS`, replace the binary, and restart the service. Future Docker upgrades require another reviewed version tag or an explicit `VAULTSYNC_NOTIFY_IMAGE` override.
**Version** — `vaultsync-notify --version` prints the installed helper version (needs no configuration). The installer shows old → new on upgrades. Docker installs pull the reviewed `2.0.2` tag and run only its resolved local content ID; failed pulls do not fall back to a stale tag. Binary installs select the newest published `notify-v*` release, require its `SHA256SUMS`, replace the binary, and restart the service. Future Docker upgrades require another reviewed version tag or an explicit `VAULTSYNC_NOTIFY_IMAGE` override.

**Diagnostics pairing comparison** — after the app has authenticated the
pending helper acceptance, run `diagnostics-docker.sh list` locally. The
Expand Down
Loading
Loading