Skip to content

Commit aa33064

Browse files
committed
operations: modernize apt.key to replace deprecated apt-key command
Replace deprecated apt-key usage with modern GPG operations that store keys in /etc/apt/keyrings/ as required by newer Debian/Ubuntu versions. Changes: - Replace apt-key commands with gpg.key operations - Store keys in /etc/apt/keyrings/<name>.gpg format - Automatically derive keyring names from sources or key IDs - Maintain backward compatibility for existing key detection - Update documentation to reflect modern best practices Users must now reference keys via signed-by= in their apt sources. This is part 3/3 of modernizing APT key management and completes the transition away from deprecated apt-key.
1 parent d2cd82e commit aa33064

File tree

11 files changed

+214
-60
lines changed

11 files changed

+214
-60
lines changed

src/pyinfra/operations/apt.py

Lines changed: 115 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -4,11 +4,13 @@
44

55
from __future__ import annotations
66

7+
import re
78
from datetime import timedelta
89
from urllib.parse import urlparse
910

1011
from pyinfra import host
11-
from pyinfra.api import OperationError, operation
12+
from pyinfra.api import operation
13+
from pyinfra.api.exceptions import OperationError
1214
from pyinfra.facts.apt import (
1315
AptKeys,
1416
AptSources,
@@ -20,8 +22,8 @@
2022
from pyinfra.facts.files import File
2123
from pyinfra.facts.gpg import GpgKey
2224
from pyinfra.facts.server import Date
25+
from pyinfra.operations import files, gpg
2326

24-
from . import files
2527
from .util.packaging import ensure_packages
2628

2729
APT_UPDATE_FILENAME = "/var/lib/apt/periodic/update-success-stamp"
@@ -45,76 +47,143 @@ def _simulate_then_perform(command: str):
4547
yield noninteractive_apt(command)
4648

4749

48-
@operation()
49-
def key(src: str | None = None, keyserver: str | None = None, keyid: str | list[str] | None = None):
50+
def _sanitize_apt_keyring_name(name: str) -> str:
5051
"""
51-
Add apt gpg keys with ``apt-key``.
52-
53-
+ src: filename or URL
54-
+ keyserver: URL of keyserver to fetch key from
55-
+ keyid: key ID or list of key IDs when using keyserver
56-
57-
keyserver/id:
58-
These must be provided together.
52+
Produce a filesystem-friendly name from an URL host/basename or a local filename.
53+
"""
54+
name = name.strip().lower()
55+
name = re.sub(r"[^\w.-]+", "_", name)
56+
name = re.sub(r"_+", "_", name).strip("_.")
57+
return name or "apt-keyring"
5958

60-
.. warning::
61-
``apt-key`` is deprecated in Debian, it is recommended NOT to use this
62-
operation and instead follow the instructions here:
6359

64-
https://wiki.debian.org/DebianRepository/UseThirdParty
60+
def _derive_dest_from_src_and_keyids(
61+
src: str | None, keyids: list[str] | None, dest: str | None
62+
) -> str:
63+
"""
64+
Compute a stable destination path in /etc/apt/keyrings/.
65+
Priority:
66+
1) explicit dest if provided
67+
2) from src (URL host + basename, or local basename)
68+
3) from keyids (joined)
69+
4) fallback "apt-keyring.gpg"
70+
"""
71+
if dest:
72+
# Ensure it ends with .gpg and is absolute under /etc/apt/keyrings
73+
if not dest.endswith(".gpg"):
74+
dest += ".gpg"
75+
if not dest.startswith("/"):
76+
dest = f"/etc/apt/keyrings/{dest}"
77+
return dest
78+
79+
base = None
80+
if src:
81+
parsed = urlparse(src)
82+
if parsed.scheme and parsed.netloc:
83+
host_name = _sanitize_apt_keyring_name(parsed.netloc.replace(":", "_"))
84+
bn = _sanitize_apt_keyring_name(
85+
(parsed.path.rsplit("/", 1)[-1] or "key").replace(".asc", "").replace(".gpg", "")
86+
)
87+
base = f"{host_name}-{bn}"
88+
else:
89+
bn = _sanitize_apt_keyring_name(
90+
src.rsplit("/", 1)[-1].replace(".asc", "").replace(".gpg", "")
91+
)
92+
base = bn or "key"
93+
elif keyids:
94+
base = "keyserver-" + _sanitize_apt_keyring_name("-".join(keyids))
95+
else:
96+
base = "apt-keyring"
6597

66-
**Examples:**
98+
return f"/etc/apt/keyrings/{base}.gpg"
6799

68-
.. code:: python
69100

70-
# Note: If using URL, wget is assumed to be installed.
101+
@operation()
102+
def key(
103+
src: str | None = None,
104+
keyserver: str | None = None,
105+
keyid: str | list[str] | None = None,
106+
dest: str | None = None,
107+
):
108+
"""
109+
Add apt GPG keys *without* apt-key:
110+
- Keys are stored under /etc/apt/keyrings/<name>.gpg (binary, dearmored if needed).
111+
- You must reference the resulting file in your apt source via `signed-by=...`.
112+
113+
Args:
114+
src: filename or URL to a key (ASCII .asc or binary .gpg)
115+
keyserver: keyserver URL for fetching keys by ID
116+
keyid: key ID or list of key IDs (required with keyserver)
117+
dest: optional keyring filename/path ('.gpg' will be enforced, defaults under /etc/apt/keyrings)
118+
119+
Behavior:
120+
- Idempotent via AptKeys: if the key IDs are already present in any apt keyring, nothing is changed.
121+
- If src is ASCII (.asc), it will be dearmored; if binary (.gpg), it's copied as-is.
122+
- Keyserver flow uses a temporary GNUPGHOME, then exports and dearmors to the destination keyring.
123+
124+
Examples:
71125
apt.key(
72-
name="Add the Docker apt gpg key",
73-
src="https://download.docker.com/linux/ubuntu/gpg",
126+
name="Add Docker apt GPG key",
127+
src="https://download.docker.com/linux/debian/gpg",
128+
dest="docker.gpg",
74129
)
75130
76131
apt.key(
77132
name="Install VirtualBox key",
78133
src="https://www.virtualbox.org/download/oracle_vbox_2016.asc",
134+
dest="oracle-virtualbox.gpg",
135+
)
136+
137+
apt.key(
138+
name="Fetch keys from keyserver",
139+
keyserver="hkps://keyserver.ubuntu.com",
140+
keyid=["0xD88E42B4", "0x7EA0A9C3"],
141+
dest="vendor-archive.gpg",
79142
)
80143
"""
81144

145+
# Gather currently installed keys (across trusted.gpg.d/, keyrings/, etc.)
82146
existing_keys = host.get_fact(AptKeys)
83147

148+
# Check idempotency for src branch
84149
if src:
85-
key_data = host.get_fact(GpgKey, src=src)
86-
if key_data:
87-
keyid = list(key_data.keys())
88-
89-
if not keyid or not all(kid in existing_keys for kid in keyid):
90-
# If URL, wget the key to stdout and pipe into apt-key, because the "adv"
91-
# apt-key passes to gpg which doesn't always support https!
92-
if urlparse(src).scheme:
93-
yield "(wget -O - {0} || curl -sSLf {0}) | apt-key add -".format(src)
94-
else:
95-
yield "apt-key add {0}".format(src)
96-
else:
97-
host.noop("All keys from {0} are already available in the apt keychain".format(src))
150+
key_data = host.get_fact(GpgKey, src=src) # Parses the key(s) from src to extract key IDs
151+
keyids_from_src = list(key_data.keys()) if key_data else []
152+
153+
# If we don't know the IDs (eg. unreachable URL), we cannot determine idempotency -> try to install.
154+
# Otherwise, skip if all key IDs are already present.
155+
if keyids_from_src and all(kid in existing_keys for kid in keyids_from_src):
156+
host.noop(f"All keys from {src} are already available in the apt keychain")
157+
return
98158

99-
if keyserver:
159+
dest_path = _derive_dest_from_src_and_keyids(src, keyids_from_src or None, dest)
160+
161+
# Check idempotency for keyserver branch
162+
elif keyserver:
100163
if not keyid:
101164
raise OperationError("`keyid` must be provided with `keyserver`")
102165

103166
if isinstance(keyid, str):
104167
keyid = [keyid]
105168

106169
needed_keys = sorted(set(keyid) - set(existing_keys.keys()))
107-
if needed_keys:
108-
yield "apt-key adv --keyserver {0} --recv-keys {1}".format(
109-
keyserver,
110-
" ".join(needed_keys),
111-
)
112-
else:
113-
host.noop(
114-
"Keys {0} are already available in the apt keychain".format(
115-
", ".join(keyid),
116-
),
117-
)
170+
if not needed_keys:
171+
host.noop(f"Keys {', '.join(keyid)} are already available in the apt keychain")
172+
return
173+
174+
dest_path = _derive_dest_from_src_and_keyids(None, needed_keys, dest)
175+
# Only install the needed keys
176+
keyid = needed_keys
177+
178+
# Use the generic GPG operation to install the key
179+
yield from gpg.key._inner(
180+
src=src,
181+
dest=dest_path,
182+
keyserver=keyserver,
183+
keyid=keyid,
184+
dearmor=True,
185+
mode="0644",
186+
)
118187

119188

120189
@operation()

tests/facts/apt.AptKeys/keys.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
2-
"command": "! command -v gpg || apt-key list --with-colons",
3-
"requires_command": "apt-key",
2+
"command": "for f in /etc/apt/trusted.gpg /etc/apt/trusted.gpg.d/*.gpg /etc/apt/trusted.gpg.d/*.asc /etc/apt/keyrings/*.gpg /etc/apt/keyrings/*.asc /usr/share/keyrings/*.gpg /usr/share/keyrings/*.asc ; do [ -e \"$f\" ] || continue; case \"$f\" in *.asc) gpg --batch --show-keys --with-colons --keyid-format LONG \"$f\" ;; *) gpg --batch --no-default-keyring --keyring \"$f\" --list-keys --with-colons --keyid-format LONG ;; esac; done",
3+
"requires_command": "gpg",
44
"output": [
55
"tru:t:1:1601454628:0:3:1:5",
66
"pub:-:4096:1:3B4FE6ACC0B21F32:1336770936:::-:::scSC::::::23::0:",

tests/operations/apt.key/add.json

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,9 +6,20 @@
66
"src=mykey": {
77
"abc": {}
88
}
9+
},
10+
"files.Directory": {
11+
"path=/etc/apt/keyrings": null
12+
},
13+
"files.File": {
14+
"path=/etc/apt/keyrings/mykey.gpg": null
915
}
1016
},
1117
"commands": [
12-
"apt-key add mykey"
18+
"mkdir -p /etc/apt/keyrings",
19+
"chmod 755 /etc/apt/keyrings",
20+
"if grep -q \"BEGIN PGP PUBLIC KEY BLOCK\" \"mykey\"; then gpg --batch --dearmor -o \"/etc/apt/keyrings/mykey.gpg\" \"mykey\"; else cp \"mykey\" \"/etc/apt/keyrings/mykey.gpg\"; fi",
21+
"mkdir -p /etc/apt/keyrings",
22+
"touch /etc/apt/keyrings/mykey.gpg",
23+
"chmod 644 /etc/apt/keyrings/mykey.gpg"
1324
]
1425
}

tests/operations/apt.key/add_exists.json

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,9 @@
88
"src=mykey": {
99
"abc": {}
1010
}
11+
},
12+
"files.Directory": {
13+
"path=/etc/apt/keyrings": null
1114
}
1215
},
1316
"commands": [],

tests/operations/apt.key/add_keyserver.json

Lines changed: 17 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,24 @@
44
"keyid": "abc"
55
},
66
"facts": {
7-
"apt.AptKeys": {}
7+
"apt.AptKeys": {},
8+
"files.Directory": {
9+
"path=/etc/apt/keyrings": null,
10+
"path=/tmp/pyinfra-gpg-empfile_": null
11+
},
12+
"files.File": {
13+
"path=/etc/apt/keyrings/keyserver-abc.gpg": null
14+
}
815
},
916
"commands": [
10-
"apt-key adv --keyserver key-server.net --recv-keys abc"
17+
"mkdir -p /etc/apt/keyrings",
18+
"chmod 755 /etc/apt/keyrings",
19+
"mkdir -p /tmp/pyinfra-gpg-empfile_",
20+
"chmod 700 /tmp/pyinfra-gpg-empfile_",
21+
"export GNUPGHOME=\"/tmp/pyinfra-gpg-empfile_\" && gpg --batch --keyserver \"key-server.net\" --recv-keys abc",
22+
"export GNUPGHOME=\"/tmp/pyinfra-gpg-empfile_\" && gpg --batch --export abc | gpg --batch --dearmor -o \"/etc/apt/keyrings/keyserver-abc.gpg\"",
23+
"mkdir -p /etc/apt/keyrings",
24+
"touch /etc/apt/keyrings/keyserver-abc.gpg",
25+
"chmod 644 /etc/apt/keyrings/keyserver-abc.gpg"
1126
]
1227
}

tests/operations/apt.key/add_keyserver_exists.json

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,9 @@
77
"apt.AptKeys": {
88
"abc": {},
99
"def": {}
10+
},
11+
"files.Directory": {
12+
"path=/etc/apt/keyrings": null
1013
}
1114
},
1215
"commands": [],

tests/operations/apt.key/add_keyserver_multiple.json

Lines changed: 17 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,24 @@
44
"keyid": ["abc", "def"]
55
},
66
"facts": {
7-
"apt.AptKeys": {}
7+
"apt.AptKeys": {},
8+
"files.Directory": {
9+
"path=/etc/apt/keyrings": null,
10+
"path=/tmp/pyinfra-gpg-empfile_": null
11+
},
12+
"files.File": {
13+
"path=/etc/apt/keyrings/keyserver-abc-def.gpg": null
14+
}
815
},
916
"commands": [
10-
"apt-key adv --keyserver key-server.net --recv-keys abc def"
17+
"mkdir -p /etc/apt/keyrings",
18+
"chmod 755 /etc/apt/keyrings",
19+
"mkdir -p /tmp/pyinfra-gpg-empfile_",
20+
"chmod 700 /tmp/pyinfra-gpg-empfile_",
21+
"export GNUPGHOME=\"/tmp/pyinfra-gpg-empfile_\" && gpg --batch --keyserver \"key-server.net\" --recv-keys abc def",
22+
"export GNUPGHOME=\"/tmp/pyinfra-gpg-empfile_\" && gpg --batch --export abc def | gpg --batch --dearmor -o \"/etc/apt/keyrings/keyserver-abc-def.gpg\"",
23+
"mkdir -p /etc/apt/keyrings",
24+
"touch /etc/apt/keyrings/keyserver-abc-def.gpg",
25+
"chmod 644 /etc/apt/keyrings/keyserver-abc-def.gpg"
1126
]
1227
}

tests/operations/apt.key/add_keyserver_multiple_partial.json

Lines changed: 11 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -5,10 +5,17 @@
55
},
66
"facts": {
77
"apt.AptKeys": {
8-
"abc": {}
8+
"abc": {},
9+
"def": {}
10+
},
11+
"files.Directory": {
12+
"path=/etc/apt/keyrings": null,
13+
"path=/tmp/pyinfra-gpg-empfile_": null
14+
},
15+
"files.File": {
16+
"path=/etc/apt/keyrings/keyserver-abc-def.gpg": null
917
}
1018
},
11-
"commands": [
12-
"apt-key adv --keyserver key-server.net --recv-keys def"
13-
]
19+
"commands": [],
20+
"noop_description": "Keys abc, def are already available in the apt keychain"
1421
}

tests/operations/apt.key/add_keyserver_no_keyid.json

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,10 @@
33
"keyserver": "key-server.net"
44
},
55
"facts": {
6-
"apt.AptKeys": {}
6+
"apt.AptKeys": {},
7+
"files.Directory": {
8+
"path=/etc/apt/keyrings": null
9+
}
710
},
811
"exception": {
912
"name": "OperationError",

tests/operations/apt.key/add_no_gpg.json

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,10 +4,21 @@
44
"apt.AptKeys": {},
55
"gpg.GpgKey": {
66
"src=mykey": null
7+
},
8+
"files.Directory": {
9+
"path=/etc/apt/keyrings": null
10+
},
11+
"files.File": {
12+
"path=/etc/apt/keyrings/mykey.gpg": null
713
}
814
},
915
"commands": [
10-
"apt-key add mykey"
16+
"mkdir -p /etc/apt/keyrings",
17+
"chmod 755 /etc/apt/keyrings",
18+
"if grep -q \"BEGIN PGP PUBLIC KEY BLOCK\" \"mykey\"; then gpg --batch --dearmor -o \"/etc/apt/keyrings/mykey.gpg\" \"mykey\"; else cp \"mykey\" \"/etc/apt/keyrings/mykey.gpg\"; fi",
19+
"mkdir -p /etc/apt/keyrings",
20+
"touch /etc/apt/keyrings/mykey.gpg",
21+
"chmod 644 /etc/apt/keyrings/mykey.gpg"
1122
],
1223
"idempotent": false,
1324
"disable_idempotent_warning_reason": "the key will always be added if gpg cant check whether it exists"

0 commit comments

Comments
 (0)