fix: address apt install review round
This commit is contained in:
parent
9cc9576116
commit
2efc76f23e
3 changed files with 61 additions and 20 deletions
26
README.md
26
README.md
|
|
@ -13,10 +13,15 @@ A command-line interface for [Forgejo](https://forgejo.org/), built with [Comman
|
||||||
|
|
||||||
### With apt (Debian/Ubuntu — recommended)
|
### With apt (Debian/Ubuntu — recommended)
|
||||||
|
|
||||||
The package is published to the Debian registry of the forge itself. The
|
The package is published to the public Debian registry of the forge itself.
|
||||||
`heavy-duty` organization is private, so installation requires a Forgejo user
|
One-time setup:
|
||||||
that belongs to the organization and a personal access token that can read its
|
|
||||||
packages. One-time setup:
|
```bash
|
||||||
|
curl -fsSL https://forgejo.heavyduty.builders/heavy-duty/stoke/raw/branch/main/scripts/install-apt.sh | bash
|
||||||
|
```
|
||||||
|
|
||||||
|
If a private registry or a `FORGE_URL=` override requires authentication,
|
||||||
|
download the installer and supply a Forgejo login and package-readable token:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
export FORGE_USER=your-forgejo-login
|
export FORGE_USER=your-forgejo-login
|
||||||
|
|
@ -27,15 +32,16 @@ sudo --preserve-env=FORGE_USER,FORGE_TOKEN bash /tmp/stoke-install-apt.sh
|
||||||
unset FORGE_TOKEN
|
unset FORGE_TOKEN
|
||||||
```
|
```
|
||||||
|
|
||||||
The installer keeps the credentials out of the source URL in a root-readable
|
The authenticated path keeps credentials out of the source URL in a
|
||||||
apt auth file. To configure that file manually before adding the source:
|
root-readable apt auth file. To configure that file manually before adding the
|
||||||
|
source:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
sudo install -d -m 0755 /etc/apt/auth.conf.d
|
sudo install -d -m 0755 /etc/apt/auth.conf.d
|
||||||
|
sudo install -m 0600 /dev/null /etc/apt/auth.conf.d/forgejo-heavy-duty.conf
|
||||||
printf 'machine forgejo.heavyduty.builders\nlogin %s\npassword %s\n' \
|
printf 'machine forgejo.heavyduty.builders\nlogin %s\npassword %s\n' \
|
||||||
"$FORGE_USER" "$FORGE_TOKEN" \
|
"$FORGE_USER" "$FORGE_TOKEN" \
|
||||||
| sudo tee /etc/apt/auth.conf.d/forgejo-heavy-duty.conf >/dev/null
|
| sudo tee /etc/apt/auth.conf.d/forgejo-heavy-duty.conf >/dev/null
|
||||||
sudo chmod 0600 /etc/apt/auth.conf.d/forgejo-heavy-duty.conf
|
|
||||||
```
|
```
|
||||||
|
|
||||||
Then add the forge's registry as an apt source:
|
Then add the forge's registry as an apt source:
|
||||||
|
|
@ -77,11 +83,13 @@ run:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
export STOKE_ALLOW_UNVERIFIED_APT=1
|
export STOKE_ALLOW_UNVERIFIED_APT=1
|
||||||
sudo --preserve-env=FORGE_USER,FORGE_TOKEN,STOKE_ALLOW_UNVERIFIED_APT \
|
curl -fsSL https://forgejo.heavyduty.builders/heavy-duty/stoke/raw/branch/main/scripts/install-apt.sh | bash
|
||||||
bash /tmp/stoke-install-apt.sh
|
|
||||||
unset STOKE_ALLOW_UNVERIFIED_APT
|
unset STOKE_ALLOW_UNVERIFIED_APT
|
||||||
```
|
```
|
||||||
|
|
||||||
|
For a private registry, re-run the downloaded installer with
|
||||||
|
`sudo --preserve-env=FORGE_USER,FORGE_TOKEN,STOKE_ALLOW_UNVERIFIED_APT` instead.
|
||||||
|
|
||||||
This exact opt-in is the only path in the installer that writes a
|
This exact opt-in is the only path in the installer that writes a
|
||||||
`[trusted=yes]` source. The installer prints the security trade-off again when
|
`[trusted=yes]` source. The installer prints the security trade-off again when
|
||||||
it takes that path.
|
it takes that path.
|
||||||
|
|
|
||||||
|
|
@ -52,10 +52,10 @@ if [ -n "$FORGE_USER" ] && [ -n "$FORGE_TOKEN" ]; then
|
||||||
forge_host="${FORGE_URL#*://}"
|
forge_host="${FORGE_URL#*://}"
|
||||||
forge_host="${forge_host%%/*}"
|
forge_host="${forge_host%%/*}"
|
||||||
$SUDO install -d -m 0755 "$APT_ETC/auth.conf.d"
|
$SUDO install -d -m 0755 "$APT_ETC/auth.conf.d"
|
||||||
|
$SUDO install -m 0600 /dev/null "$AUTH"
|
||||||
printf 'machine %s\nlogin %s\npassword %s\n' \
|
printf 'machine %s\nlogin %s\npassword %s\n' \
|
||||||
"$forge_host" "$FORGE_USER" "$FORGE_TOKEN" \
|
"$forge_host" "$FORGE_USER" "$FORGE_TOKEN" \
|
||||||
| $SUDO tee "$AUTH" >/dev/null
|
| $SUDO tee "$AUTH" >/dev/null
|
||||||
$SUDO chmod 0600 "$AUTH"
|
|
||||||
CURL_AUTH=(--netrc-file "$AUTH")
|
CURL_AUTH=(--netrc-file "$AUTH")
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
|
@ -108,7 +108,7 @@ ensure_nodejs_source() {
|
||||||
}
|
}
|
||||||
|
|
||||||
echo "Adding APT source for $FORGE_URL/$OWNER ..."
|
echo "Adding APT source for $FORGE_URL/$OWNER ..."
|
||||||
$SUDO install -d -m 0755 "$APT_ETC/keyrings"
|
$SUDO install -d -m 0755 "$APT_ETC/keyrings" "$APT_ETC/sources.list.d"
|
||||||
$SUDO curl "${CURL_AUTH[@]}" -fsSL "$FORGE_URL/api/packages/$OWNER/debian/repository.key" | $SUDO tee "$KEYRING" >/dev/null
|
$SUDO curl "${CURL_AUTH[@]}" -fsSL "$FORGE_URL/api/packages/$OWNER/debian/repository.key" | $SUDO tee "$KEYRING" >/dev/null
|
||||||
echo "deb [signed-by=$KEYRING] $FORGE_URL/api/packages/$OWNER/debian $DISTRIBUTION $COMPONENT" \
|
echo "deb [signed-by=$KEYRING] $FORGE_URL/api/packages/$OWNER/debian $DISTRIBUTION $COMPONENT" \
|
||||||
| $SUDO tee "$LIST" >/dev/null
|
| $SUDO tee "$LIST" >/dev/null
|
||||||
|
|
@ -134,19 +134,13 @@ fi
|
||||||
# is not the key algorithm, because the registry serves an RSA-2048 signing
|
# is not the key algorithm, because the registry serves an RSA-2048 signing
|
||||||
# key (`gpg --list-packets` on repository.key reports `algo 1` with a
|
# key (`gpg --list-packets` on repository.key reports `algo 1` with a
|
||||||
# 2048-bit pkey[0]). Try the properly signed source first so this heals
|
# 2048-bit pkey[0]). Try the properly signed source first so this heals
|
||||||
# automatically once the forge is fixed. Only that signature-error class, plus
|
# automatically once the forge is fixed. Only the exact live sqv failure, plus
|
||||||
# the user's exact opt-in, permits an unverified source; auth, network, and
|
# the user's exact opt-in, permits an unverified source; auth, network, and
|
||||||
# other failures must leave verification enabled and retain apt's diagnostic.
|
# other failures must leave verification enabled and retain apt's diagnostic.
|
||||||
if update_output="$(update_only_source "$LIST" 2>&1)"; then
|
if update_output="$(update_only_source "$LIST" 2>&1)"; then
|
||||||
printf '%s\n' "$update_output"
|
printf '%s\n' "$update_output"
|
||||||
else
|
else
|
||||||
update_status=$?
|
update_status=$?
|
||||||
if ! grep -Eiq \
|
|
||||||
'NO_PUBKEY|EXPKEYSIG|BADSIG|signatures? (could not|couldn.t) be verified|signature (verification )?(failed|failure|error|invalid)|repository .*not signed|is not signed' \
|
|
||||||
<<<"$update_output"; then
|
|
||||||
printf '%s\n' "$update_output" >&2
|
|
||||||
exit "$update_status"
|
|
||||||
fi
|
|
||||||
if ! grep -Fqi '/usr/bin/sqv' <<<"$update_output" \
|
if ! grep -Fqi '/usr/bin/sqv' <<<"$update_output" \
|
||||||
|| ! grep -Fqi 'Malformed MPI' <<<"$update_output"; then
|
|| ! grep -Fqi 'Malformed MPI' <<<"$update_output"; then
|
||||||
printf '%s\n' "$update_output" >&2
|
printf '%s\n' "$update_output" >&2
|
||||||
|
|
|
||||||
|
|
@ -16,12 +16,13 @@ const SCRIPT = path.join(__dirname, '..', 'scripts', 'install-apt.sh');
|
||||||
// sourceUpdateError stderr and exit 100 for the first signed stoke update
|
// sourceUpdateError stderr and exit 100 for the first signed stoke update
|
||||||
// forgeUser/token private-registry credentials
|
// forgeUser/token private-registry credentials
|
||||||
// allowUnverified explicit HTTPS-only integrity opt-in
|
// allowUnverified explicit HTTPS-only integrity opt-in
|
||||||
|
// precreateSourcesDir whether the throwaway apt root already has sources.list.d
|
||||||
// The apt-cache stub localizes the "Candidate:" label unless LC_ALL=C is set,
|
// The apt-cache stub localizes the "Candidate:" label unless LC_ALL=C is set,
|
||||||
// so every scenario doubles as a regression test for locale-safe parsing.
|
// so every scenario doubles as a regression test for locale-safe parsing.
|
||||||
const cleanups = [];
|
const cleanups = [];
|
||||||
process.on('exit', () => { for (const dir of cleanups) fs.rmSync(dir, { recursive: true, force: true }); });
|
process.on('exit', () => { for (const dir of cleanups) fs.rmSync(dir, { recursive: true, force: true }); });
|
||||||
|
|
||||||
function runScenario({ candInitial, candAfterUpdate, candAfterNodesource, preexistingNodesourceList, releaseStatus, sourceUpdateError, forgeUser, forgeToken, allowUnverified }) {
|
function runScenario({ candInitial, candAfterUpdate, candAfterNodesource, preexistingNodesourceList, releaseStatus, sourceUpdateError, forgeUser, forgeToken, allowUnverified, precreateSourcesDir = true }) {
|
||||||
const root = fs.mkdtempSync(path.join(os.tmpdir(), 'stoke-apt-test-'));
|
const root = fs.mkdtempSync(path.join(os.tmpdir(), 'stoke-apt-test-'));
|
||||||
cleanups.push(root);
|
cleanups.push(root);
|
||||||
const bin = path.join(root, 'bin');
|
const bin = path.join(root, 'bin');
|
||||||
|
|
@ -29,7 +30,7 @@ function runScenario({ candInitial, candAfterUpdate, candAfterNodesource, preexi
|
||||||
const aptEtc = path.join(root, 'etc', 'apt');
|
const aptEtc = path.join(root, 'etc', 'apt');
|
||||||
fs.mkdirSync(bin, { recursive: true });
|
fs.mkdirSync(bin, { recursive: true });
|
||||||
fs.mkdirSync(state, { recursive: true });
|
fs.mkdirSync(state, { recursive: true });
|
||||||
fs.mkdirSync(path.join(aptEtc, 'sources.list.d'), { recursive: true });
|
if (precreateSourcesDir) fs.mkdirSync(path.join(aptEtc, 'sources.list.d'), { recursive: true });
|
||||||
fs.writeFileSync(path.join(state, 'candidate'), candInitial);
|
fs.writeFileSync(path.join(state, 'candidate'), candInitial);
|
||||||
if (preexistingNodesourceList !== undefined) {
|
if (preexistingNodesourceList !== undefined) {
|
||||||
fs.writeFileSync(path.join(aptEtc, 'sources.list.d', 'nodesource.list'), preexistingNodesourceList);
|
fs.writeFileSync(path.join(aptEtc, 'sources.list.d', 'nodesource.list'), preexistingNodesourceList);
|
||||||
|
|
@ -43,6 +44,20 @@ function runScenario({ candInitial, candAfterUpdate, candAfterNodesource, preexi
|
||||||
// Force the non-root path so every mutation goes through the sudo stub.
|
// Force the non-root path so every mutation goes through the sudo stub.
|
||||||
stub('id', 'echo 1000');
|
stub('id', 'echo 1000');
|
||||||
stub('sudo', 'SUDO_ACTIVE=1 exec "$@"');
|
stub('sudo', 'SUDO_ACTIVE=1 exec "$@"');
|
||||||
|
stub('tee', [
|
||||||
|
'for destination in "$@"; do',
|
||||||
|
' case "$destination" in',
|
||||||
|
' */auth.conf.d/forgejo-*.conf)',
|
||||||
|
' mode="$(stat -c %a "$destination" 2>/dev/null || true)"',
|
||||||
|
' if [ "$mode" != 600 ]; then',
|
||||||
|
' echo "credential destination was not mode 0600 before write" >&2',
|
||||||
|
' exit 78',
|
||||||
|
' fi',
|
||||||
|
' ;;',
|
||||||
|
' esac',
|
||||||
|
'done',
|
||||||
|
'exec /usr/bin/tee "$@"',
|
||||||
|
].join('\n'));
|
||||||
// Registry Release-file probes (URLs under /dists/) answer with the
|
// Registry Release-file probes (URLs under /dists/) answer with the
|
||||||
// scenario's HTTP status; everything else is a key fetch.
|
// scenario's HTTP status; everything else is a key fetch.
|
||||||
stub('curl', [
|
stub('curl', [
|
||||||
|
|
@ -196,6 +211,16 @@ test('registry Release file present: proceeds with the install', () => {
|
||||||
assert.match(s.aptGetLog, /install -y stoke/);
|
assert.match(s.aptGetLog, /install -y stoke/);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('fresh apt root creates sources.list.d before writing the forge source', () => {
|
||||||
|
const s = runScenario({
|
||||||
|
candInitial: '22.23.1-1nodesource1',
|
||||||
|
precreateSourcesDir: false,
|
||||||
|
});
|
||||||
|
assert.equal(s.res.status, 0, s.res.stderr);
|
||||||
|
assert.match(s.forgeList, /\[signed-by=/);
|
||||||
|
assert.match(s.aptGetLog, /install -y stoke/);
|
||||||
|
});
|
||||||
|
|
||||||
test('signature verification failure refuses by default and removes the forge source', () => {
|
test('signature verification failure refuses by default and removes the forge source', () => {
|
||||||
const s = runScenario({
|
const s = runScenario({
|
||||||
candInitial: '22.23.1-1nodesource1',
|
candInitial: '22.23.1-1nodesource1',
|
||||||
|
|
@ -235,6 +260,20 @@ test('opt-in cannot bypass a missing signing key', () => {
|
||||||
assert.doesNotMatch(s.aptGetLog, /install -y stoke/);
|
assert.doesNotMatch(s.aptGetLog, /install -y stoke/);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('opt-in cannot bypass sqv output without the known Malformed MPI failure', () => {
|
||||||
|
const failure = 'W: OpenPGP signature verification failed: Sub-process /usr/bin/sqv returned an error code (1): unexpected packet';
|
||||||
|
const s = runScenario({
|
||||||
|
candInitial: '22.23.1-1nodesource1',
|
||||||
|
sourceUpdateError: failure,
|
||||||
|
allowUnverified: '1',
|
||||||
|
});
|
||||||
|
assert.notEqual(s.res.status, 0);
|
||||||
|
assert.match(s.res.stderr, new RegExp(failure.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')));
|
||||||
|
assert.match(s.forgeList, /\[signed-by=/);
|
||||||
|
assert.doesNotMatch(s.forgeList, /trusted=yes/);
|
||||||
|
assert.doesNotMatch(s.aptGetLog, /install -y stoke/);
|
||||||
|
});
|
||||||
|
|
||||||
test('unrecognized opt-in value is rejected before configuring apt', () => {
|
test('unrecognized opt-in value is rejected before configuring apt', () => {
|
||||||
const s = runScenario({
|
const s = runScenario({
|
||||||
candInitial: '22.23.1-1nodesource1',
|
candInitial: '22.23.1-1nodesource1',
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue