apt-thirdparty://<app-id> handler to install software from third-party APT repositories in one click.
Example:
apt-thirdparty://my-app
The handler does not execute arbitrary URLs from links.
It reads a local allowlist configuration per app:
/etc/apt-thirdparty-handler/apps.d/<app-id>.conf- optionally refreshes this allowlist from a signed remote bundle
Each config defines:
- the GPG key URL to import
- the
deb ...repository line to write - the package name to install
By default, app definitions come from the signed remote allowlist.
You can update the allowlist without shipping a new .deb by publishing a signed apps.tar.
Install with your package manager UI or with:
sudo apt install ./apt-thirdparty-handler_1.0_all.debAfter installation, test:
xdg-open 'apt-thirdparty://my-app'Refresh signed whitelist manually:
sudo apt-thirdparty-handler --refresh-indexsudo dpkg -r apt-thirdparty-handlerPurge package config files:
sudo dpkg -P apt-thirdparty-handlerFrom the repository root:
./build-apt-thirdparty-handler.shOutput:
apt-thirdparty-handler_<version>_all.deb(generated at repository root)
Build with a custom version:
./build-apt-thirdparty-handler.sh 1.1Example:
APP_ID="my-app"
DISPLAY_NAME="My App"
KEY_URL="https://example.com/repo/signing-key.asc"
KEYRING_PATH="/etc/apt/keyrings/my-app.gpg"
LIST_FILE_PATH="/etc/apt/sources.list.d/my-app.list"
REPO_LINE="deb [arch=amd64 signed-by=/etc/apt/keyrings/my-app.gpg] https://example.com/repo stable main"
PACKAGE_NAME="my-app"For Ubuntu-derivative repositories (like PPAs), REPO_LINE may include a codename placeholder:
REPO_LINE="deb [signed-by=/etc/apt/keyrings/my-app.gpg] https://ppa.launchpadcontent.net/vendor/ppa/ubuntu __UBUNTU_CODENAME__ main"Supported placeholders:
__UBUNTU_CODENAME__${UBUNTU_CODENAME}$UBUNTU_CODENAME
At install time, the handler resolves this to the Ubuntu base codename detected on the machine (works for Ubuntu and Linux Mint).
Once installed:
- click an
apt-thirdparty://my-applink - confirm the initial action
- authenticate (pkexec)
- if no local app config exists yet, the script refreshes the signed whitelist, then asks a second explicit confirmation with exact package/repository details before installing
- install repository and package
- allowlist-based app IDs (
apt-thirdparty://<id>) - no dynamic parameters executed from the URL
- strict app ID validation (
[a-z0-9][a-z0-9._-]*) - signed whitelist refresh (GPG detached signature)
- automatic first-run key bootstrap over HTTPS (can be overridden with preinstalled trusted keyring)
Edit:
/etc/apt-thirdparty-handler/whitelist.conf
Minimum config:
WHITELIST_BASE_URL="https://example.com/apt-thirdparty"
WHITELIST_REFRESH_INTERVAL_SEC=86400The handler will fetch:
https://example.com/apt-thirdparty/apps.tarhttps://example.com/apt-thirdparty/apps.tar.aschttps://example.com/apt-thirdparty/whitelist-signing.pub(only if local trusted keyring is missing)
By default, this step is automatic.
If /usr/share/apt-thirdparty-handler/trustedkeys.gpg is missing, the handler downloads:
${WHITELIST_BASE_URL}/whitelist-signing.pub(orWHITELIST_KEY_URLif explicitly configured),
then dearmors it and installs the trusted keyring before signature verification.
Manual installation is still possible:
curl -fsSL https://example.com/apt-thirdparty/whitelist-signing.pub \
| gpg --dearmor \
| sudo tee /usr/share/apt-thirdparty-handler/trustedkeys.gpg >/dev/nullWhitelist publication is managed in phramusca.github.io.
Prepare an apps.d directory containing one *.conf per app in that repository, e.g.:
apps.d/
my-app.confBuild + sign the bundle from phramusca.github.io:
./tools/build-whitelist-bundle.sh ./.apt-thirdparty/apps.d "you@example.com" ./apt-thirdpartyThis produces:
apt-thirdparty/apps.tarapt-thirdparty/apps.tar.asc
Publish both files at your WHITELIST_BASE_URL.
Create key:
gpg --full-generate-keyRecommended choices for this project:
- key type:
ECC (sign only) - curve:
Curve 25519(default) - expiration: e.g.
1y(renewable) - set a strong passphrase (20+ chars, unique, stored in a password manager)
Even for local publishing, a passphrase is recommended: if the machine is compromised, an unprotected private signing key can be used immediately.
Export public key for clients:
gpg --armor --export "you@example.com" > whitelist-signing.pubUse one identity on every machine where you publish; copy the secret key (it stays passphrase-protected).
On the source machine, find the key ID
gpg --list-secret-keys --with-fingerprintthen:
gpg --armor --export-secret-keys KEYID > signing-secret.ascTransfer signing-secret.asc over a trusted channel (encrypted USB, scp, etc.—not email or a git repo). On the destination:
gpg --import signing-secret.asc
shred -u signing-secret.asc # or rm after a secure wipeKeep a separate encrypted backup if you need recovery after disk loss.
With a short expiry (e.g. 1y), plan before the key expires.
Extend the same key (recommended if nothing is compromised): fingerprint stays the same.
gpg --edit-key KEYID
# gpg> expire
# gpg> save
gpg --armor --export "you@example.com" > whitelist-signing.pubRepublish whitelist-signing.pub. Clients must refresh the public key material in trustedkeys.gpg (re-import the updated armored public key into that keyring, or remove the keyring only if you are sure the next download from WHITELIST_KEY_URL is trustworthy).
Switch to a new signing key: generate a new key; during overlap, publish both public keys in one file, e.g. gpg --armor --export OLDKEYID NEWKEYID > whitelist-signing.pub; sign bundles with the new secret key; after clients have updated, remove the old public key from the published file and from client keyrings.