Repository navigation
Security
-
An open websocket outlived a revoked session or a suspended account (#72). A long-lived
connection through the proxy is re-authorized every two minutes, but the check reused who the
person was when the connection opened, so signing them out everywhere or suspending them (R-048,
R-049) left it open. It now authenticates the connection's session or token again each time, and
closes it with a policy-violation close frame when that no longer works. -
BuildKit no longer runs with seccomp unconfined (#130). Pando starts BuildKit itself, as
pando-buildkiton the Docker it runs apps on (R-111), under a seccomp profile compiled into Pando:
Docker's default plus only the calls rootless BuildKit needs, creating namespaces, mounting inside
them, and each build step's keyring and hostname. Builds can no longer load BPF programs, read
performance counters, load kernel modules, set the clock or reboot. AppArmor stays unconfined.
BuildKit is v0.33.1 (from v0.17.2), pinned by digest, and Postgres is pinned by digest too, which
Dependabot now keeps current. The compose file no longer has abuildkitservice;
PANDO_BUILDKIT_ADDRESSstill points Pando at a BuildKit you run instead. -
Setting up a new installation needs the setup token from Pando's log (#130). Before, whoever
reached the console first became the administrator. Pando now prints a one-timesetup_tokenon
the startup warning that the installation is not set up yet, and the setup form asks for it.
pando admin setup-token, run where Pando runs, makes a new one if the log line is gone.
PANDO_ADMIN_PASSWORDinstalls are unaffected. -
The bundled Postgres no longer defaults to the password
pando(#130, R-401). A one-shot
secretsservice generates one on the first start and keeps it in the newpando-secretsvolume;
Postgres and Pando read it from there as a file, throughPOSTGRES_PASSWORD_FILEand the new
PANDO_DATABASE_PASSWORD_FILE.POSTGRES_PASSWORDset before the first start is used instead. -
On Linux, run Pando on rootless Docker (#130, R-402). Pando holds the Docker socket, so on
rootful Docker a compromise of Pando is root on the host.docs/rootless.md
sets up rootless Docker for it, and the compose file mounts whichever socket
PANDO_DOCKER_SOCKETnames. Installation, Capacity shows whether the runtime is rootless. -
Pando refuses to deploy where Docker would drop an app's CPU or memory limits (#130, R-240).
Docker accepts limits its host cannot apply and starts the container without them, which rootless
Docker does unless the cpu cgroup controller is delegated to its user. The refusal says which
limit and how to fix it. Before, the app ran with the whole host. -
An app is held to its disk limit (#130, R-403). Every app has one (10 GiB unless set), and
until now it was only counted against the host's disk. Pando now measures each running app's
containers and volumes every 10 minutes, tells its owner at 90% and when it goes over, and stops
it if the next reading is still over. Its data is kept; starting it clears the stop. The console
shows "Stopped over disk limit", and a visitor is told why.
Added
-
The audit log can be streamed to a SIEM and exported (#129, design 12).
GET /audit/stream,
pando audit tail --followand thepando_read_audit_streamMCP tool read every event in commit
order from a cursor, and resume from it after a restart, an upgrade or with several replicas without
missing one. Audit sinks are a new adapter category,audit_sink:syslog(RFC 5424 over TLS)
andhttps, with presets for Splunk HEC, Datadog, Elastic, Sumo Logic and Azure Monitor (Microsoft
Sentinel), sending Pando's native JSON or OCSF 1.3.0 (docs/audit-formats.md). Each sink's
backlog, last error and any gap are on the Audit log screen,GET /audit/sinksand
pando audit sinks; one that keeps failing for a day is turned off and its administrators are told.
A month is not archived while an enabled sink has not been sent it.GET /audit/exportand
pando audit exportdownload any range of the live log. Sending the log off the installation needs
the newinstall.audit.exportpermission, which the Administrator role holds. -
Every audit event now records its outcome (success, denied or failed), the client's address and
user agent, and the acting person's name and email. Behind a proxy, list the proxy in
PANDO_SERVER_TRUSTED_PROXIESfor the client's own address to be recorded rather than the proxy's. -
More is audited: the end of a terminal session (
app.exec.end), every deploy's outcome
(deploy.finish), what a rollback rolled back from, which fields a policy or spec change touched,
opening an app's logs (app.logs.read), and reading the audit log itself (audit.read,
audit.export,audit.archive.download). -
An install image registry (#72).
PANDO_REGISTRY_URLand its credential name a registry that
builds are pushed to, by digest, for a runtime that pulls rather than imports; runtimes and the
scanner pull with the registry's credential, and a deleted app's images are deleted from it.
docker-compose.registry.ymladds a registry to the Compose topology. A single-host install needs
none and keeps importing builds directly, unlessPANDO_REGISTRY_ALWAYSis set. See
docs/reference.mdfor the settings. The registry is an adapter category of its own
(image_registry, kindsociandecr, #153), configured like any adapter from the console, the
API (POST /adapters),pando adapter addand MCP, and used from the moment it is saved; the
password is stored encrypted and never shown.PANDO_REGISTRY_*declares one, read-only while it is
set. -
A DR bundle now includes uploaded source (#72). The registry's images are not in it, so an uploaded
app's archive is what it is rebuilt from after a restore. -
disable_anonymous_denial_audithost policy setting (#72), also settable as
PANDO_POLICY_DISABLE_ANONYMOUS_DENIAL_AUDIT. Off by default: a visitor who is not signed in being
refused a private app is still written to the audit log asauthz.denied. Turned on, those rows
stop; refusals of signed-in people and tokens are always recorded. -
Event subscriptions (#50). Subscribe to what happens in Pando — deploys started, succeeded and failed,
app state and health, security scores, shares and grants, backups, sign-ins, policy, adapter health,
upgrades — on one app or the whole installation, from the console's new Events screen,
pando subscriptions,/api/v1/subscriptionsor MCP. Each is sent to a webhook, as JSON signed with a
per-subscription key (Pando-Signature), or through a notification adapter. Deliveries are retried for
about a day, every attempt is recorded and can be sent again, and an endpoint that fails everything for
a day is turned off and its owner told. The catalog isdocs/events.md. New
permissioninstall.events.manage, held by Administrator, for install-wide subscriptions; anyone who
can see an app can subscribe to it. Webhooks cannot reach private, loopback or link-local addresses
unless host policy's newallow_private_webhooksis on. -
Notification adapters for email (SMTP, which covers SendGrid, Mailgun and Amazon SES), Slack,
Microsoft Teams, Discord and ntfy. Email reaches people at the address on their account; the others
post to a channel and are sent to only by subscriptions. -
Notification preferences, under Settings: which of Pando's own notifications reach you, on the
console and by email. "An app was shared with you" is off by default; the launcher tile is still the
notification. -
A notifications inbox: a bell beside Settings with the number unread,
pando notifications listand
GET /api/v1/me/notifications. Console notifications were recorded before and shown nowhere. -
Each app has an Events tab: its recent events, and its subscriptions. Also
pando events --appand
GET /api/v1/apps/{id}/events. -
A webhook can send the request its receiver expects: method, content type, headers of its own (stored
encrypted, for an API key the receiver needs) and a body template. Deliveries, notifications and the
inbox link to the app whenexternal_urlis set. -
A failed deploy tells the app's owner and whoever started it; a failed backup tells the owner and
whoever manages backups. An account token can own a subscription, within its own grants. -
In-place upgrades (#53). Pando can upgrade itself from the Updates screen,
POST /upgradeor
pando upgrade: it verifies the new image's signature against Pando's release workflow, takes a full
backup (or records that you skipped it), and starts a helper that replaces Pando's container, waits
for the new version to be ready, and puts the previous version and its database back if it is not.
Off until the deployment turns it on: set the image to a moving tag such astrypando/pando:latest
andPANDO_POLICY_UPGRADE_IN_PLACE: "true", or use Let Pando upgrade itself on the Policy screen.
New permissioninstall.upgrade, held by Administrator and denied to agent tokens by default. Host
policy gainsupgrade_in_place,auto_upgrade_patchesandmaintenance_windowfor automatic patch
upgrades.pando upgrade lastandGET /upgrade/lastsay how the last one went. -
pando self-updatereplaces a CLI installed from a release archive or withgo install, after
checking the release's signature. Holders ofinstall.upgradeare notified once of each new release. -
Update checks (#53). Pando asks GitHub at startup and every six hours whether a newer release is
out, sending only its version. The new Updates screen,GET /updates,pando updatesand MCP's
pando_get_updatesshow the version running, the latest release, and the changelog of every version
in between, with security fixes and breaking changes marked, then the command that upgrades this
installation. Host policy gainsdisable_update_check, which stops the check and every request it
sends, andupdate_channel(stableorprerelease), on the Policy screen under Updates.
The CLI warns when it and the server differ in major or minor version, and names the command that
upgrades it the way it was installed. Upgrading from inside Pando is not part of this release. -
Pando refuses to start against a database a newer version of Pando migrated, and says to run that
version or restore the backup taken before the upgrade. It used to fail with "Database migration
failed." and restart in a loop, leaving every app unreachable. -
Audit log retention (#60). The live audit log keeps three months by default; an older month is
archived as gzipped JSON lines with a manifest (row count, first and last event, time range, SHA-256),
read back and checked, and only then removed — and the removal is itself an audit event. Archives are
kept under/var/lib/pando/audit-archives(server.audit_archive_dir) or exported to a backup
destination. Host policy gainsaudit_retention_months(at least 3),audit_archive(keep,
exportoroff) andaudit_archive_destination, on the Policy screen under Audit log.
Archived months are listed and downloaded from the Audit log screen,GET /audit/archives,
pando audit archivesandpando audit archives download, and listed by MCP's
pando_list_audit_archives. The account Pando serves traffic as still cannot change or remove any
audit event: a month is removed by a separate database role, through a function that refuses one
under three months old or without a verified archive. -
Single sign-on and provisioning (#51). Connect an identity provider over OpenID Connect or
SAML 2.0 on the new Sign-in screen, with presets for Okta, Microsoft Entra ID, Google
Workspace, Keycloak and Authentik; test it with a real sign-in that shows every claim the provider
sent and what Pando would do with it; then turn it on. Providers are added and changed without a
restart. SCIM 2.0 per provider creates, updates and suspends accounts and sets group membership
at once; deprovisioning suspends and never deletes. A provider's groups become synced groups that
you give roles and app access, or link to an existing Pando group. Accounts are made at a first
sign-in only when a provider is set to (off by default), and an administrator links an identity to
an existing account — email linking is opt-in and only on a verified email. Host policy gains
Turn off password sign-in and Don't create accounts at first sign-in. The CLI has
pando identity-provider(pando idp); setup per provider is indocs/identity-providers.md. -
Waiting for detection (#80).
GET /apps/{id}/detection?wait=30answers as soon as detection
reaches a new stage or finishes, and while it runs says which stage it is on and for how long.
pando app detection <app> --waitprints each stage and exits 0 when the app is ready to accept,
2 when there are questions to answer and 1 when detection failed or was blocked;pando app add --waitdoes the same. MCP'spando_get_detectiontakeswait_seconds. Apps inGET /apps,
GET /apps/{id}andpando app listsay where their detection is, so a draft says why. -
Who used an app is in the audit log:
app.use, once per visit (a browser session, or a token's use
within twelve hours), with the first page visited. Visitors who are not signed in are recorded too,
with the address Pando saw; host policy's Don't record visits from people who aren't signed in
(disable_anonymous_use_audit) turns that off. Audit search can now answer "who accessed this app". -
AI functions beyond detection (#74): drafting roles and groups, drafting host policy, searching the
audit log with a question, and answering "How can I…" from the API, CLI and MCP reference. Each
proposes and a person applies. Each AI function is assigned to one AI adapter, optionally on a model
of its own, chosen when editing the adapter. -
OpenAI and local-model AI adapters, beside Anthropic. The local adapter talks to any server that
speaks the OpenAI API, such as Ollama, LM Studio, llama.cpp's server or vLLM, and sends nothing to a
provider. -
Adapters, and the AI functions each handles, can be declared in the config file's
adapters:
section, with credentials named by environment variable or file. Declared ones are read-only in the
console, API, CLI and MCP. -
A security scan now shows while it runs: in the app's Security section and in the app list's
Security column, wherever the scan was started (a deploy, detection, the CLI, MCP or Scan
now). The API reports it asscanning_sinceonGET /apps/{id}/securityand
security_scanningon each app inGET /apps. -
Egress rules (#79). Host policy sets where apps may connect out to: anywhere, anywhere except
a denylist, or only an allowlist, plus a separate switch that blocks private addresses (the local
network, loopback, the cloud metadata address). An app can add or remove entries, or keep a list of
its own that narrows the installation's. Changes that loosen the installation's rules are allowed by
permission, only after a deploy approval, or never, as policy says. The plan and the app's
Settings tab show the merged rules and where each came from. On Docker, a restricted app runs
on a network with no route out and reaches the internet only through a per-app egress gateway, as
HTTP or HTTPS viaHTTP_PROXY/HTTPS_PROXY; an app with no restriction runs exactly as before. -
Deploy approval (#39). Policy can require approval for every app's deploys or for chosen apps,
and an app can require it of itself. A deploy then waits until enough people holding
install.deploys.approve(administrators) orapp.deploy.approve(no built-in role) approve it,
with a configurable count and expiry (default one approval, seven days). Rolling back to a revision
that ran needs no approval; auto-deploy is refused while approval is required. The console has an
Approvals screen; the API,pando approvalsand three MCP tools do the same. Agent tokens
cannot approve by default. -
Permissions on every app, one at a time (#81). Each app permission has an installation-wide
counterpart that grants it on every app, including apps added later:install.apps.deploy,
install.apps.logs.read,install.apps.secrets.readand so on. A group gets "read every app's logs"
once instead of a grant on each app. Three new built-in installation roles: App viewer (see every
app and its logs), App manager (Owner on every app, without approving deploys) and Auditor
(every app's view and logs, and the audit log). A change allowed through one of these, rather than a
grant on the app, is written to the audit log asauthz.install_wide, naming the grant. -
An OpenAPI 3.1 document for the API,
docs/openapi.json(R-261). Every endpoint with its
parameters, request body, responses and the error envelope with every error code, and which
credential each takes. It is generated from the code that serves the API bymake reference, and
the build fails when it is out of date.
Changed
-
Apps with no deployment instructions are planned by Railpack instead of nixpacks (#172).
nixpacks is in maintenance mode, and Railpack is its maintainers' replacement. The plan Pando
shows and stores for such an app is now Railpack's JSON build plan (each step's commands, the
images it runs on, the start command) rather than a Dockerfile, and it is still editable before
it runs. It is built through Railpack's frontend image inside Pando's own rootless BuildKit, with
no privileged container and no runtime socket. Railpack's images are pinned by digest. Railpack
defaults Node to the current LTS and Python to 3.13, reads.node-version,.ruby-versionand the
Gemfile'srubyline itself, starts a Mix project from its release, and builds C sources with
the compiler in its builder image. Laravel, Jekyll, static sites, Node servers, Go, .NET and JVM
apps are still planned by Pando's own Dockerfiles, as before. Planning now looks language versions
up from the package indexes, so an installation that cannot reach the internet cannot plan these
apps; building already needed it. The image is about 110 MB larger, for the mise binary Railpack
resolves versions with, and now carriesgitandbash, which mise needs to list Python
releases. nixpacks is no longer in the image. -
API tokens are stored as HMAC-SHA-256 under a key rather than argon2id (#93). A token is 256 random
bits Pando made, so the slow hash cost 64 MiB and tens of milliseconds on every request a token made
for no benefit. The key is a new file,/var/lib/pando/token.key(PANDO_SERVER_TOKEN_KEY_PATH),
created on first start and kept out of the database so that a database dump alone cannot be used to
check a guessed token. Every replica must read the same file, and a DR bundle includes it. Existing
tokens keep working and are rewritten on their next use; passwords are still argon2id. A token's
last-used time is now recorded to the minute rather than written on every request. -
The proxy reuses its connections to an app instead of opening one per request, finds the app a
request is for with one indexed lookup instead of reading every app's spec, and the app screen's
permission check reads grants and policy once instead of once per permission (#72). -
install.apps.manageis gone; the App manager role holds its thirteen verbs.install.apps.view
now means seeing every app only, and every role that held it also holdsinstall.apps.logs.read, so
what it allowed is unchanged. -
An install has at most one AI adapter per provider, and an AI adapter has no default: each AI
function is off until assigned. The existing AI adapter keeps plan repair, answering detection's
questions and plan revision on upgrade; the new functions start off. -
Pando sets a
pando_visit_<app>cookie on responses from apps, to recognize a visit. Like every
cookie namedpando_…, it never reaches the app. -
Pando moved to the
trypandoGitHub organization. The repository is
github.com/trypando/pando, the Go module path isgithub.com/trypando/pando, and the Homebrew tap
istrypando/tap/pando. Oldgithub.com/bemeek-io/...URLs redirect. -
app.egress.overrideis nowapp.egress.loosen, and a newapp.egress.tighten(Owner and
Operator) changes an app's egress within the installation's rules. An app's own allowlist now
narrows the installation's instead of replacing it.
Fixed
- A build refused by the security scan or the port check could still run (#72, R-146). Builds were
loaded aspando/<app>:latestand deployments recorded that tag, so a refused build moved it, and a
container the reconciler recreated afterwards ran the refused image. Each build is now tagged with
its deployment and recorded by its image ID. Older tags of an app's builds are removed past the
newest eleven. - The identity assertion and
X-Pando-Emailheader for somebody signed in with a session carried no
email or name (R-054). They now do. - An app that failed did not tell its owner (#50). The reconciler wrote the notification and had no
notifier wired to send it. It now goes to the owner on the console, and by email where that is set up. - A security-score warning reached only the console (#50). It now reaches the owner on every channel
that reaches people, as their notification preferences allow. - Redeploying an unchanged source ran the security scan again (#84). A deploy of a commit,
image or uploaded archive that already has a successful scan by the configured scanner uses that
scan and says so in the deploy log ("Using the security scan of … (source unchanged)"). That
includes a new revision of the same source, which used to be refused as never scanned where a
minimum score was set, and everypando deploy .of an unchanged directory. A scan that failed,
or one by a different scanner, is not reused. "Scan now" in app settings always scans. - A Laravel app failed to build. Laravel 13 needs PHP 8.3, the build installed an older PHP, and
the build log said onlysyntax error, unexpected token "{"(#58). A Laravel app is now built on
the newest PHP itscomposer.jsonallows, with the extensions it requires, its Vite assets built,
and served by Apache frompublic/; it runs its migrations before it starts. When a dependency
needs a PHP version or extension the build does not have, the build fails saying which. Deploying
one now asks forAPP_KEY, which the app cannot serve a page without;php artisan key:generate --showprints one. Detect a Laravel app again to pick this up. pando deploy <dir>printed blank lines where detection's questions belonged. It read a field
the server never sends. The questions are now printed exactly as Pando wrote them.- A static site's pages redirected to the wrong port. Opening a folder without its trailing slash
(/solutions) on an app reached at an address with a port, such aslocalhost:9001, sent the
browser to the same path on port 80, where nothing answers (#67). The redirect now keeps the
address the browser used. Redeploy a static site to pick this up. - A Jekyll site was served as its source. A GitHub Pages site deployed and looked healthy, but its
Markdown pages showed as raw Markdown or failed to load (#67). Pando now builds a Jekyll site with
Jekyll before serving it. A GitHub Pages site (no Gemfile, or one naminggithub-pages) is built
the way GitHub builds it, with the currentgithub-pagesgem; any other Jekyll site with its own
Gemfile, without its development and test gems. Either is served at the root of the app's address
whateverbaseurlthe site sets for GitHub. Detect the app again to get the new build plan. - Daily app backups could silently not happen (#87). Two apps that each kept a volume with the
same name (datais common) shared one record of it: the second app was never backed up, the
first app's backups copied the second app's data, and the second app lost the protection against
recreating a lost volume empty. Each app now has its own record, and Pando repairs existing records
from what the runtime reports for each running app within a minute of starting. An app whose configuration was saved without
a backup count is now backed up with the default of 7 rather than skipped. A whole-installation
backup names each app's volumes separately; older backups still restore. - Every daily backup attempt is recorded, and the last one — taken, skipped or failed, with why and
what to do — is shown on the app's overview and on Backups. The server log says "took a rolling
backup" only when one was taken.
Upgrade notes
-
Existing apps keep the nixpacks plan they were deployed with (#172). A plan is part of the
spec (R-020), so rebuilding an app replays its stored nixpacks Dockerfile unchanged, and it still
builds. To move an app to Railpack, re-run its detection (R-022) and accept the new proposal. -
On Ubuntu 23.10 and later, builds need the host to allow unprivileged user namespaces. Rootless
BuildKit cannot start otherwise, and this was as true of the compose file'sbuildkitservice as
it is of Pando's own. As root:sysctl -w kernel.apparmor_restrict_unprivileged_userns=0, and the
same line in a file under/etc/sysctl.d/. The build service's health check now says so. -
The compose file's
buildkitservice is gone; Pando startspando-buildkitinstead. After
docker compose up -dwith the new file, remove the old one with
docker compose up -d --remove-orphans. The oldbuildkit-cachevolume is no longer used and can
be removed withdocker volume rmonce the new build cache has filled. With
docker-compose.registry.yml, take the new copy of it too: it gives Pando the registry's CA for its
BuildKit, rather than configuring abuildkitservice. -
An installation upgraded before anyone set it up prints its setup token on the first start of the
new version. Scripts that callPOST /api/v1/setupmust now sendsetup_token; take it from
pando admin setup-token, which prints the token alone on stdout. -
An installation made with the old compose file keeps its database password only if you say what
it is. Its Postgres was created withpando, or with yourPOSTGRES_PASSWORD, and the new
compose file would otherwise generate a different one that the database does not accept. Before
the firstdocker compose up -dwith the new file, put that password in a.envfile beside it
(POSTGRES_PASSWORD=pandoif you never set one); thesecretsservice writes it to the
password file, and later starts read it from there. Back up thepando-secretsvolume with
postgres-datafrom then on. -
If Docker on your host cannot apply CPU or memory limits, deploys are now refused until it can;
the refusal says what to change. To check before upgrading, rundocker infoand look for
warnings about CPU quotas, memory limits or cgroups. After upgrading, Installation, Capacity shows
cpu_limitsandmemory_limitsunder Show details. -
Migration 000067 adds columns to
audit_eventswithout rewriting the rows already there; events
written before it have no outcome, source or actor name, and the stream reads them first, in the
order they were written. The audit log grows faster than before, because reading it, opening an
app's logs and the end of each terminal session are now recorded. -
Audit events older than three months leave the live log at the first daily pass after
upgrading, archived under/var/lib/pando/audit-archivesfirst. To keep everything in the live
log, start the upgraded server withPANDO_POLICY_AUDIT_ARCHIVE=off(oraudit_archive: offin
host policy) and nothing is archived or removed. Migration 000041 partitionsaudit_eventsby
month by copying it, which takes a while on a large log, and adds a second restricted database
role,pando_audit_archiver; an external database must let Pando's account create it, as it
already createspando_app. -
Migration 000040 replaces
install.apps.managewith its thirteeninstall.apps.*verbs in every role
that held it, custom roles included, addsinstall.apps.logs.readwhereverinstall.apps.viewwas,
and adds the App viewer, App manager and Auditor roles. A custom role already named one of those is
renamed with " (custom)". Scripts that create a role withinstall.apps.manageneed the new verbs,
or a grant ofrole_app_manager. -
Migration 000039 renames
app.egress.overridetoapp.egress.loosenin every role that held it,
custom roles included, and in host policy's disabled-verb lists. Scripts that name the old verb need
the new one. -
An install that set
egress_allowlistis now held to it. That setting was never enforced
before; after upgrading, apps reach only what it lists. Clear it, or setegress_mode, before
upgrading if that is not what you want. Rules take effect at each app's next deploy. -
Restricted egress on Docker needs the gateway image: Pando uses its own container's image, or the
runtime adapter'segress_gateway_image. Pando run directly on a host has neither, so a deploy with
a restriction is refused with a message saying why. -
Migration 000037 adds the external identity tables and the
effective_group_membersview, which
authorization now reads for group membership. No existing behavior changes until a provider is
added. To use identity providers behind a proxy, setPANDO_SERVER_EXTERNAL_URL: the addresses you
register with a provider are built from it. -
pando admin enable-password-sign-inis new: the way back in from the host if password sign-in is
off and no provider works. -
An install with more than one AI adapter of the same provider must remove all but one before
upgrading; the migration stops with a message naming the provider otherwise. -
go install github.com/bemeek-io/pando/cmd/pando@latestkeeps installing the last version
released before the move. Usego install github.com/trypando/pando/cmd/pando@latest. -
A Homebrew install from the old tap:
brew untap bemeek-io/tap && brew install trypando/tap/pando. -
cosignverification indocs/releasing.mdaccepts both the old and the new signing identity, so
releases from before the move still verify.