GPG Signing and Key Management for OCI-Backed Flatpak Repositories#
This guide covers how GPG signing works for OCI-backed Flatpak repositories in AetherPak — from the underlying containers/image simple-signing protocol to day-to-day key management practices that protect your users without creating operational risk for you.
1. GPG Signing in the Flatpak Ecosystem#
Flatpak applications distributed through OCI registries require a separate signing mechanism outside the registry itself. GPG signatures ensure that users can verify the authenticity and integrity of each application image before installing it, protecting against tampered or malicious content.
Traditional Flatpak/OSTree Signing#
Flatpak remotes served as OSTree repositories have long used GPG signing on every commit. When a user adds a remote with flatpak remote-add --gpg-import=key.gpg, the public key is pinned locally to the repository. On each install or update operation, the Flatpak client verifies the detached GPG signature on the OSTree commit before accepting the content .
This model works well for traditional static-HTTP OSTree repositories, where the signature is stored alongside the commit objects.
The OCI Challenge#
OCI-backed Flatpak remotes (oci+https://) store application content as OCI images in a container registry like GitHub Container Registry (GHCR). The OCI registry has no native GPG signature mechanism—it supports technologies like Cosign and keyless signing, but Flatpak's OCI verifier does not use any of those .
This creates a fundamental problem: how do you attach and verify GPG signatures for OCI images when the registry itself doesn't understand them?
The Solution: containers/image Simple-Signing Lookaside#
The solution is to store signatures separately from the OCI registry, at a well-known HTTP path called a "signature lookaside." This is the same protocol used by Red Hat's Atomic Host and by podman and skopeo with --signature-policy .
The Flatpak OCI verifier uses a --signature-lookaside flag to specify the base URL where signatures live. Signatures are fetched over plain HTTP, independent of the OCI registry .
⚠️ Warning: The
--signature-lookasideflag requires Flatpak ≥ 1.17. On older clients, signature verification is not available, and users must use--no-gpg-verifywhen adding the remote.
The Atomic Container Signature Format#
Each signature is a JSON payload following the "atomic container signature" format :
{
"critical": {
"type": "atomic container signature",
"image": {
"docker-manifest-digest": "sha256:<hex>"
},
"identity": {
"docker-reference": "<registry>/<repo>:<tag>"
}
},
"optional": {
"creator": "aetherpak",
"timestamp": 1704067200
}
}
Key fields:
critical.type: Must be exactly"atomic container signature"critical.image.docker-manifest-digest: The OCI manifest digest (SHA-256) of the image being signedcritical.identity.docker-reference: The full image reference including registry, repository, and tagoptional: An object containing metadata like the creator and timestamp. This field is required by the format—verifiers such asskopeoandpodmanreject payloads without it .
This JSON payload is serialized to bytes and then GPG-signed using a detached binary signature. The signature file is stored at:
<lookaside-base>/<repo>@sha256=<digest>/signature-1
Multiple signers can produce additional signatures (signature-2, signature-3, etc.) for the same image.
Flatpak's OCI Verification Flow#
When a user installs or updates an application from an OCI-backed Flatpak remote, the verification flow works as follows :
-
Read the index: The client fetches
index/staticfrom GitHub Pages to discover the OCI image digest for the requested app. -
Resolve the image digest: The client determines which OCI manifest digest corresponds to the app, architecture, and channel requested.
-
Fetch signatures: The client constructs the signature URL from the
--signature-lookasidebase and the image digest, then fetchessignature-1,signature-2, etc. until it receives an HTTP 404. -
Verify the payload: For each signature:
- The client GPG-verifies the detached signature against the pinned public key
- Verifies that
critical.image.docker-manifest-digestmatches the digest about to be fetched - Verifies that
critical.identity.docker-referencematches the expected<registry>/<repo>:<tag>
-
Fetch the image: Only if at least one signature is valid does the client proceed to fetch and install the OCI image layers from the registry.
Why AetherPak Uses GPG-Only Signing#
AetherPak enforces GPG signing exclusively—not Cosign, not keyless signing—because that is what Flatpak's OCI verifier supports . The containers/image library and its simple-signing specification expect GPG detached signatures; there is no current mechanism for Flatpak to verify signatures from other technologies in an OCI context.
Verification Example#
A complete verified remote add for a signed AetherPak repository looks like this :
# Fetch the public key
curl -fsSLO https://aetherpak.github.io/actions-demo/sigs/key.asc
# Add the remote with signature verification
flatpak remote-add --user \
--gpg-import=key.asc \
--signature-lookaside=https://aetherpak.github.io/actions-demo/sigs \
actions-demo oci+https://aetherpak.github.io/actions-demo
# Install with full verification
flatpak install --user actions-demo org.gnome.Sudoku
On older Flatpak versions (< 1.17), the --signature-lookaside flag is not recognized, and users must use --no-gpg-verify instead :
flatpak remote-add --if-not-exists --user --no-gpg-verify \
actions-demo oci+https://aetherpak.github.io/actions-demo
2. How AetherPak Uses Lookaside Signatures#
AetherPak implements GPG-based signing for OCI-backed Flatpak remotes using the containers/image simple-signing lookaside protocol. Signing is GPG-only — Cosign and keyless signing are not used, because Flatpak's OCI verifier only supports the simple-signing lookaside with GPG . This section details how signatures are created, stored, and integrated into the published repository.
Signing During push-oci#
When --gpg-key is provided, aetherpak push-oci signs the image manifest in-process . The signing workflow:
- Construct the payload —
push-ocibuilds an atomic container signature JSON payload:
{
"critical": {
"type": "atomic container signature",
"image": {
"docker-manifest-digest": "sha256:..."
},
"identity": {
"docker-reference": "ghcr.io/owner/repo:org_gnome_Sudoku-stable-x86_64"
}
},
"optional": {
"creator": "aetherpak",
"timestamp": 1735689600
}
}
The critical.identity.docker-reference field encodes the OCI tag, and the critical.image.docker-manifest-digest is the SHA-256 digest of the manifest. The optional object is required by the format — verifiers such as skopeo and podman reject payloads without it .
-
Sign the payload —
push-ocicallsSignWithAll()to generate a detached GPG signature for the JSON payload with each loaded key . If multiple keys are loaded, each produces its own signature. -
Write signatures — signatures are written into the per-cell record directory at
sigs/<repo>@sha256=<digest>/signature-N:
_records/<app-id>-<arch>/
record.json
labels.json
sigs/
<repo>@sha256=<hex>/
signature-1
signature-2 # if multiple keys were used
OCI Tag Encoding (Critical Detail)#
NOTE: Flatpak's OCI verifier imposes a subtle constraint on image tags that affects signature verification. Understanding this constraint is critical when hand-crafting signatures or debugging signature failures.
Flatpak's OCI verifier builds the expected identity as <registry>/<repo> (bare, without a tag) and strips the tag from the signature's embedded docker-reference field using a [0-9A-Za-z_-]-only regex . App IDs containing dots (e.g., org.gnome.Sudoku) would fail this strip, leaving the identity tagged, and the verifier would reject an otherwise-valid signature.
AetherPak works around this by encoding . as _ in the OCI tag portion :
safeAppID := strings.ReplaceAll(opts.AppID, ".", "_")
tag := fmt.Sprintf("%s-%s-%s", safeAppID, opts.Branch, opts.Arch)
// Result: org_gnome_Sudoku-stable-x86_64
The canonical app ID is preserved in the org.flatpak.ref OCI label, so Flatpak clients still resolve the correct app identity from the index. The tag encoding is purely for signature verification.
Lookaside Directory Structure#
When aetherpak build-site runs, it assembles the signature lookaside from all per-cell records :
_site/
sigs/
key.asc # ASCII-armored public key
signing.json # Manifest for the landing page
<oci-repo>@sha256=<digest>/
signature-1 # Binary GPG signature (per image)
signature-2 # Additional signature if multiple keys
Each signature file is a detached binary GPG signature of the atomic container signature payload. The signature is stored at a content-addressed path derived from the OCI repository name and the manifest digest. Because paths are content-addressed by digest, parallel cells writing signatures for different images never collide .
signing.json Format#
The signing.json manifest is read by the landing page to show verified-install commands :
{
"enabled": true,
"lookaside": "sigs",
"publicKey": "sigs/key.asc",
"fingerprint": "ABCD1234ABCD1234ABCD1234ABCD1234ABCD1234",
"remoteName": "owner-repo"
}
When signing is disabled: {"enabled": false}.
build-site Assembly and Backfilling#
aetherpak build-site merges all per-cell sigs/ subdirectories into _site/sigs/ . This is straightforward for signatures written in the current run, but index/static is cumulative — it is seeded from the previous Pages deployment, so the current run may reference images whose signatures were written in earlier runs.
Backfilling solves this problem: for each image in the index, build-site checks if the signature file exists locally. If not, it fetches the signature from the deployed Pages URL:
- For each image in the index, build the expected signature path:
sigs/<repo>@sha256=<digest>/signature-1 - If the file does not exist locally, fetch it from
<pages-url>/sigs/<repo>@sha256=<digest>/signature-1 - Fetch
signature-2,signature-3, ... until HTTP 404 is returned - Write each fetched signature to
_site/sigs/at the same relative path
Backfilling is parallelized with a concurrency limit of 10 HTTP requests . Path traversal protection with isUnderDir prevents malicious paths from escaping the site directory .
This ensures that the deployed _site/sigs/ directory contains signatures for all images listed in the index, even if those images were published in earlier workflow runs.
.flatpakref Embedding#
The .flatpakref format supports embedding both the GPG public key and the signature lookaside URL, enabling self-contained verified installs . When signing is enabled, aetherpak build-site writes each .flatpakref file with:
[Flatpak Ref]
Title=My App
Name=org.example.App
Branch=stable
Url=oci+https://owner.github.io/repo
RuntimeRepo=https://dl.flathub.org/repo/flathub.flatpakrepo
GPGKey=mQINBGX...base64-encoded-binary-keyring...==
SignatureLookaside=https://owner.github.io/repo/sigs
IsRuntime=false
GPGKey: base64-encoded binary keyring (not ASCII-armored) — exported byExportBase64PublicKeyRing()SignatureLookaside: points at thesigs/base path on the deployed Pages URL
Clicking "Install" in the browser opens the .flatpakref, adds the remote with the key and lookaside configured, and installs the app in one step. No manual key import is required.
.flatpakrepo Limitation#
WARNING: The
.flatpakrepoformat cannot embedGPGKey(for lookaside-based signing) orSignatureLookaside. Installing via.flatpakrepoleaves signature verification incomplete.
After adding a remote via .flatpakrepo, users must manually configure the signature lookaside:
flatpak remote-modify --user \
--signature-lookaside=https://<owner>.github.io/<repo>/sigs \
<owner>-<repo>
The landing page shows this remote-modify command prominently when signing is enabled. This limitation is a Flatpak design constraint — the .flatpakrepo format predates OCI-backed remotes and has no field for the lookaside URL.
Adding a Signed Remote from the Command Line#
For users who prefer the terminal, the full verified remote-add command is :
# Fetch the public key
curl -fsSLO https://<owner>.github.io/<repo>/sigs/key.asc
# Add the remote with signature verification (requires Flatpak >= 1.17)
flatpak remote-add --user \
--gpg-import=key.asc \
--signature-lookaside=https://<owner>.github.io/<repo>/sigs \
<owner>-<repo> oci+https://<owner>.github.io/<repo>
# Install the app
flatpak install --user <owner>-<repo> org.example.App
The --signature-lookaside flag requires Flatpak >= 1.17 . On older versions, users must add the remote with --no-gpg-verify and signatures will not be checked.
Signing Mode#
The signing input controls whether signing is required, optional, or disabled:
| Mode | Behavior |
|---|---|
auto (default) | Sign if gpg-private-key is provided; skip silently if not |
gpg | Require a key; fail the workflow if no key is set |
off | Never sign, even if a key is provided |
In auto mode, the workflow degrades gracefully: if no key is set, unsigned images are pushed and signing.json is written with {"enabled": false}. In gpg mode, a missing key causes the workflow to fail, enforcing a policy that all published images must be signed.
3. Using GPG Subkeys for Resilient Key Management#
The Subkey Security Model#
The recommended pattern for production deployments: keep the primary GPG key offline (air-gapped storage), and use only a signing subkey in CI/GitHub Actions .
- Primary key: Has Certify (C) capability only — it can sign other keys and subkeys, but not data
- Signing subkey: Has Sign (S) capability only — used to sign OCI image signatures
- CI isolation: GitHub Actions only ever receives the subkey's secret material, not the primary key
Why Use Subkeys?#
Subkeys provide three critical advantages:
- Limited blast radius: If CI is compromised, only the signing subkey is exposed. The primary key remains safe in offline storage.
- Revocation without re-keying: Revoke a compromised subkey → generate a new signing subkey → publish the updated public key → users who have already trusted the primary key automatically trust the new subkey (same primary fingerprint, same pinned key in their Flatpak remote).
- Primary key safety: The primary key's certification capability is only needed when generating, revoking, or extending the expiration of subkeys — never during day-to-day signing operations.
TIP: For any production deployment, use subkeys instead of a single flat key. This is the industry-standard practice for code signing, package distribution, and release engineering.
AetherPak's Full Subkey Support#
AetherPak's signing implementation uses the ProtonMail go-crypto library and fully supports subkey-only secret material:
NewSigner(): Loads both armored and binary key material; accepts keys with only subkey secret material (the primary key can be a stub or public-only)decryptEntity(): Explicitly iteratesentity.Subkeysand callsDecrypt()on each encrypted subkey — not just the primary key- Validation : Checks that all subkeys are unlocked, returning the specific error message
"GPG subkey at index %d (subkey %d) is encrypted/locked (passphrase required)"if any remain locked SignWithAll(): The go-crypto library selects the appropriate signing subkey automatically when signing
This means you can export just the signing subkey secret material for CI, while keeping the primary key entirely offline.
Step-by-Step Guide#
1. Generate a Primary Key (Certification-Only)#
Generate a primary key with only the Certify capability:
gpg --batch --gen-key <<EOF
%no-protection
Key-Type: RSA
Key-Length: 4096
Key-Usage: cert
Name-Real: My App Releases
Name-Email: releases@example.org
Expire-Date: 0
%commit
EOF
The Key-Usage: cert line creates a certification-only primary key. The %no-protection line creates an unencrypted key; omit it if you prefer passphrase protection (and store the passphrase as a GitHub secret).
2. Add a Signing Subkey#
Add a signing subkey to the primary key:
# Quick method (non-interactive)
gpg --quick-add-key <FINGERPRINT> rsa4096 sign 0
Or interactively:
gpg --edit-key releases@example.org
gpg> addkey
# Choose: (4) RSA (sign only)
# Key size: 4096
# Expiration: 0 (no expiration)
gpg> save
The subkey has the Sign (S) capability and can sign data, but cannot certify other keys.
3. Export Subkey-Only Secret Material for CI#
Export only the signing subkey's secret material — the primary key secret is not included:
gpg --armor --export-secret-subkeys releases@example.org > signing-subkey.asc
Verify the export shows a stub primary key:
gpg --show-keys signing-subkey.asc
Expected output:
sec# rsa4096/0x1234567890ABCDEF 2026-06-04 [C]
Key fingerprint = ABCD 1234 ...
uid My App Releases <releases@example.org>
ssb rsa4096/0xFEDCBA0987654321 2026-06-04 [S]
sec#— The#symbol means the primary key secret is not present (stub only)ssb— Signing subkey secret is present
WARNING: The
signing-subkey.ascfile contains secret key material. Store it securely as a GitHub secret and never commit it to version control.
Store the contents of signing-subkey.asc as the GPG_PRIVATE_KEY GitHub secret .
4. Export the Public Key for Users#
Export the full public key (both primary and subkey public portions):
gpg --armor --export releases@example.org > key.asc
This key.asc file gets published at sigs/key.asc when users add your remote. It contains no secret material and is safe to distribute publicly.
5. Keep the Primary Key Offline#
After exporting the subkey, remove the primary key secret from your working keyring and store it in secure offline storage:
# Back up the full primary key (including secret material)
gpg --armor --export-secret-keys releases@example.org > primary-key-backup.asc
# Delete the secret key from the keyring
gpg --delete-secret-key releases@example.org
# Re-import just the subkey secret material for daily use
gpg --import signing-subkey.asc
Store primary-key-backup.asc in:
- Hardware security key (YubiKey, Nitrokey, etc.)
- Encrypted USB drive in a safe
- Password manager with secure file storage
- Paper backup (printable key format)
The primary key is only needed when managing subkeys (adding, revoking, extending expiration).
Rotating a Signing Subkey#
When a subkey needs to be rotated (e.g., due to suspected compromise or routine key hygiene):
-
Retrieve the primary key from offline storage and import it:
gpg --import primary-key-backup.asc -
Revoke the old signing subkey:
gpg --edit-key releases@example.org gpg> key 1 # Select the first subkey (the signing subkey) gpg> revkey # Revoke it # Confirm the revocation gpg> save -
Add a new signing subkey:
gpg --quick-add-key <FINGERPRINT> rsa4096 sign 0 -
Export the new subkey secret for CI:
gpg --armor --export-secret-subkeys releases@example.org > new-signing-subkey.asc -
Update the
GPG_PRIVATE_KEYGitHub secret with the contents ofnew-signing-subkey.asc. -
Re-export and publish the updated public key:
gpg --armor --export releases@example.org > key.ascCommit the updated
key.ascor let the next publish workflow deploy it automatically.
Users do not need to re-import the public key manually. The primary key fingerprint is unchanged, and Flatpak trusts any subkey certified by the trusted primary key. This is the key advantage of subkeys over full key rotation (see Section 4 for the limitations of full primary key rotation in Flatpak).
Verifying Which Key Signed an Image#
To verify the signature on a specific image and see which key signed it:
# Fetch a signature file from the lookaside
curl -fsSL "https://<owner>.github.io/<repo>/sigs/<oci-repo>@sha256=<digest>/signature-1" \
-o signature-1
# Verify the GPG signature (uses keys in your local keyring)
gpg --verify signature-1
Output shows the signing key ID:
gpg: Signature made Wed 04 Jun 2026 12:34:56 PM UTC
gpg: using RSA key FEDCBA0987654321
gpg: Good signature from "My App Releases <releases@example.org>" [unknown]
To verify against a specific keyring (without importing to your main keyring):
gpg --keyring ./key.asc --verify signature-1
To check the fingerprint published in signing.json:
curl -fsSL "https://<owner>.github.io/<repo>/sigs/signing.json"
The fingerprint field contains the 40-character hexadecimal fingerprint of the primary key.
Passphrase-Protected Subkeys#
AetherPak supports passphrase-protected keys . Both the primary key and all subkeys are decrypted using the same passphrase :
jobs:
publish:
uses: aetherpak/actions/.github/workflows/publish.yml@v3
with:
manifest-path: org.example.App.json
signing: gpg
secrets:
gpg-private-key: ${{ secrets.GPG_PRIVATE_KEY }}
gpg-private-key-passphrase: ${{ secrets.GPG_PRIVATE_KEY_PASSPHRASE }}
When generating a passphrase-protected key, omit the %no-protection line from the gpg --batch --gen-key command. When adding a passphrase-protected subkey, GPG will prompt for the primary key's passphrase to unlock it before adding the subkey.
The passphrase is securely cleared from memory immediately after unlocking the keys .
4. Why Automated Key Rotation Doesn't Work in Flatpak Today#
Flatpak's GPG signing model provides strong cryptographic verification for app installs, but it has a fundamental limitation: there is no automated mechanism for rotating trusted keys. Once a user adds a Flatpak remote, the trusted GPG key is pinned locally, and flatpak update will never fetch or trust a new key on its own.
The Core Limitation: Pinned Keys#
When you add a Flatpak remote with flatpak remote-add, the client stores the trusted GPG key material in the local OSTree repository configuration — either in /var/lib/flatpak/repo/ (system) or ~/.local/share/flatpak/repo/ (user). From that moment on, the Flatpak client only trusts signatures from that specific key.
flatpak update only fetches app content. It never fetches new key material, and it has no mechanism to automatically trust a new signing key pushed by the remote. There is no in-band protocol for a repository to communicate "here is my new trusted key" to clients.
The Only Rotation Path: Manual User Action#
The only way to rotate a signing key is to ask every user to manually import the new key:
flatpak remote-modify --gpg-import=new-key.asc <remote-name>
This requires the user to:
- Download the new public key from a trusted source
- Verify its authenticity (ideally via a secure channel, such as a GPG-signed announcement)
- Run the
remote-modifycommand
This is a manual, out-of-band process. There is no way for the repository itself to push a new key to clients.
AetherPak's Key Rotation Process#
AetherPak's documentation acknowledges this limitation. To rotate a signing key in an AetherPak repository :
- Generate a new GPG key
- Replace the
GPG_PRIVATE_KEYrepository secret with the new key - Re-publish every channel — rotation only fully takes effect once every image still listed in the index has been re-signed with the new key
The ARCHITECTURE.md confirms: "Rotation therefore only fully takes effect once every still-listed image has been re-signed."
The new public key replaces the old one in the sigs/ directory and in signing.json on the next deployment. However, users with the old key pinned in their local configuration will see signature verification failures until they manually import the new key.
Importantly, the backfill mechanism does not help with key rotation. AetherPak's backfillSignatures function fetches historical signatures from the deployed Pages site, but it copies them as-is — it does not re-sign old images with the new key. Backfill is purely a deployment optimization to avoid re-uploading signatures that are already published.
Flathub: A Real-World Example#
Flathub, the central Flatpak app store, uses a single long-lived signing key for all images in its repository. This key has been in use since Flathub's launch, and there has never been a published key rotation.
If Flathub needed to rotate its primary signing key today — for example, due to compromise or as part of a security policy — hundreds of millions of Flatpak installations worldwide would require a manual remote-modify command. There is no way for Flathub to push a new key to clients automatically.
This is not a hypothetical problem. It is a fundamental design limitation of Flatpak's current trust model.
Fundamental Design Limitations#
The Flatpak trust model lacks several mechanisms that would enable automated key rotation:
| Mechanism | What it does | Flatpak support |
|---|---|---|
| Certificate chains | A new key can be certified by an existing trusted key | ❌ No (except GPG subkeys, see below) |
| Key transparency logs | Append-only tamper-evident log of key operations (like Certificate Transparency for TLS) | ❌ No |
| TOFU with rotation | Trust-on-first-use with automatic rotation capability (like SSH with certificate authorities) | ❌ No |
| Signed metadata updates | The index itself is signed, allowing a trusted new key to be embedded in the update stream | ❌ No (the index/static file is not signed) |
Without these mechanisms, Flatpak has no way to communicate trust in a new key without manual user intervention.
Contrast with Other Ecosystems#
APT (Debian/Ubuntu)#
APT has historically used apt-key to manage repository signing keys, which had its own problems (a global keyring). Modern Debian (since Bookworm) uses signed-by in sources.list, which pins specific keys per repository. APT key rotation still requires administrator intervention, but the tooling makes it explicit:
- The Release file is signed with the repository key
- Distributions pre-install trusted keyrings
- Updates to the keyring package can add new keys (but cannot revoke old ones without manual intervention)
APT does not have automated rotation, but it has better tooling support for key pinning and explicit key management.
The Update Framework (TUF)#
TUF is a framework specifically designed for secure software update systems. It was built to solve the key rotation problem and is used by PyPI, Docker Content Trust, conda-forge, and others.
TUF provides:
- Hierarchical key roles: root, targets, snapshot, timestamp keys with different trust levels
- Online key rotation: Clients automatically trust new keys certified by the root
- Threshold signing: Require N of M keys to sign, protecting against single-key compromise
- Key expiration: Keys have mandatory expiration dates, forcing periodic rotation
- Offline root key: The root key can be kept offline and only used to certify new online keys
TUF's design makes automated key rotation seamless. A new signing key certified by the root is automatically trusted by all clients on their next update check.
Flatpak has none of these mechanisms.
Cosign / sigstore#
The sigstore ecosystem (Cosign, Rekor, Fulcio) provides keyless signing: ephemeral keys are generated via OIDC (e.g., GitHub Actions), signatures are logged in a transparency log (Rekor), and verification is based on certificate identity rather than long-lived keys.
This eliminates the need for key rotation entirely — there are no long-lived keys to rotate.
But Flatpak's OCI verifier does not support Cosign or keyless signing. AetherPak explicitly uses the GPG-based containers/image simple-signing lookaside, which Flatpak's OCI support implements .
The Partial Mitigation: Subkeys#
This is where the strategy from Section 3 provides real value:
NOTE: New signing subkeys under the same primary key are trusted automatically. The primary key fingerprint doesn't change, so Flatpak's pinned key trusts any subkey certified by it. This is the only form of automated rotation available in Flatpak's current trust model.
Cycling signing subkeys periodically (e.g., annually) is a good practice that provides a limited form of forward secrecy:
- The primary key is kept offline
- The signing subkey is used in CI
- A compromised signing subkey can be revoked and replaced without manual user intervention — users automatically trust the new subkey
Important caveat: This only works for subkey rotation. If the primary key itself is compromised, manual user intervention is unavoidable.
Practical Implications for Repository Maintainers#
The lack of automated key rotation leads to several practical recommendations for AetherPak (and Flatpak) repository maintainers:
1. Design for Never Rotating the Primary Key#
Treat your primary GPG key like a Certificate Authority root key. It should have a lifetime measured in decades, not years.
- Keep it offline (air-gapped machine, hardware token, or secure cold storage)
- Back it up securely (encrypted, redundant storage)
- Document its fingerprint prominently (README, website, landing page)
- Plan for it to last the entire lifetime of your repository
2. Use Subkeys for All CI Signing#
This is the most important mitigation available within the current Flatpak trust model. See Section 3 for detailed subkey setup instructions.
- Generate a primary key with Certify (C) capability only
- Generate a signing subkey with Sign (S) capability
- Export only the signing subkey secret material for CI
- Rotate the signing subkey periodically (e.g., annually)
This strategy provides the only form of automated rotation that Flatpak supports.
3. If Primary Key Compromise Occurs#
If your primary key is compromised, you must:
- Revoke the key immediately: Publish the revocation certificate to keyservers and your repository landing page
- Generate a new primary key: Follow the full key generation process from Section 3
- Update all distribution channels: Replace the fingerprint in your README, installation instructions,
.flatpakreffiles, and landing page - Notify users explicitly: Post a security advisory on GitHub, your website, and any relevant communication channels
- Document the manual rotation command: Users must run:
curl -fsSLO https://<your-pages-url>/sigs/key.asc
flatpak remote-modify --user --gpg-import=key.asc <remote-name>
- Old images become unverified: Any images signed with the compromised key will fail signature verification until they are re-published with the new key
4. Document Your Key Fingerprint#
Make it easy for security-conscious users to verify they have the right key. AetherPak's signing.json file includes the fingerprint , and it is displayed on the generated landing page. Consider also:
- Including the fingerprint in your repository README
- Publishing it via multiple channels (website, Mastodon, email signature)
- Signing the fingerprint with a separate, well-known key (e.g., your personal GPG key)
5. Consider Signing Mode#
AetherPak supports three signing modes:
auto(default): Sign if a GPG key is provided; skip silently if notgpg: Require a key; fail the workflow if no key is setoff: Never sign, even if a key is provided
Using signing: gpg (not auto) ensures the workflow fails if the key is not configured, preventing accidentally publishing unsigned images after a key rotation or configuration error.
Key Rotation Comparison Table#
| Feature | Flatpak (primary key) | Flatpak (subkey) | APT | TUF |
|---|---|---|---|---|
| Automated rotation | ❌ No | ✅ Yes (automatic subkey trust) | ❌ No | ✅ Yes |
| Requires manual user action | ✅ Yes (remote-modify) | ❌ No | ✅ Yes (admin) | ❌ No |
| Supports key delegation | ❌ No | ⚠️ Limited (subkeys only) | ❌ No | ✅ Yes |
| Supports threshold signing | ❌ No | ❌ No | ❌ No | ✅ Yes |
| Key expiration enforcement | ⚠️ Optional (GPG expiry) | ⚠️ Optional (GPG expiry) | ⚠️ Optional | ✅ Mandatory |
| Transparency logs | ❌ No | ❌ No | ❌ No | ✅ Yes (TUF spec) |
Summary#
Automated key rotation does not work in Flatpak's current trust model because:
- Keys are pinned at
remote-addtime and never updated automatically - There is no in-band mechanism for a remote to push new trusted keys
- The only rotation path is manual:
flatpak remote-modify --gpg-import
The only form of automated rotation available is GPG subkey rotation under the same primary key. This is why Section 3's subkey strategy is critical: it is the only way to achieve limited key rotation without requiring manual user intervention.
For repository maintainers, the practical advice is:
- Use GPG subkeys for all signing (primary key offline, signing subkey in CI)
- Rotate signing subkeys periodically (e.g., annually)
- Design your primary key to last the lifetime of your repository
- Document your fingerprint prominently
- If the primary key is compromised, notify users and document the manual rotation process
Until Flatpak adopts a more sophisticated trust framework (such as TUF or a signed index with key delegation), manual primary key rotation remains unavoidable.