fix: preserve apt signature verification #38
2 changed files with 57 additions and 3 deletions
|
|
@ -18,12 +18,21 @@ FORGE_URL="${FORGE_URL:-https://forgejo.heavyduty.builders}"
|
||||||
OWNER="${OWNER:-heavy-duty}"
|
OWNER="${OWNER:-heavy-duty}"
|
||||||
DISTRIBUTION="${DISTRIBUTION:-stable}"
|
DISTRIBUTION="${DISTRIBUTION:-stable}"
|
||||||
COMPONENT="${COMPONENT:-main}"
|
COMPONENT="${COMPONENT:-main}"
|
||||||
|
FORGE_USER="${FORGE_USER:-}"
|
||||||
|
FORGE_TOKEN="${FORGE_TOKEN:-}"
|
||||||
# Where apt configuration lives; overridable so tests can run against a
|
# Where apt configuration lives; overridable so tests can run against a
|
||||||
# throwaway directory instead of the real /etc/apt.
|
# throwaway directory instead of the real /etc/apt.
|
||||||
APT_ETC="${STOKE_APT_ETC:-/etc/apt}"
|
APT_ETC="${STOKE_APT_ETC:-/etc/apt}"
|
||||||
|
|
||||||
KEYRING="$APT_ETC/keyrings/forgejo-$OWNER.asc"
|
KEYRING="$APT_ETC/keyrings/forgejo-$OWNER.asc"
|
||||||
LIST="$APT_ETC/sources.list.d/forgejo-$OWNER.list"
|
LIST="$APT_ETC/sources.list.d/forgejo-$OWNER.list"
|
||||||
|
AUTH="$APT_ETC/auth.conf.d/forgejo-$OWNER.conf"
|
||||||
|
|
||||||
|
if { [ -n "$FORGE_USER" ] && [ -z "$FORGE_TOKEN" ]; } \
|
||||||
|
|| { [ -z "$FORGE_USER" ] && [ -n "$FORGE_TOKEN" ]; }; then
|
||||||
|
echo "error: FORGE_USER and FORGE_TOKEN must be set together" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
SUDO=""
|
SUDO=""
|
||||||
if [ "$(id -u)" -ne 0 ]; then
|
if [ "$(id -u)" -ne 0 ]; then
|
||||||
|
|
@ -31,6 +40,18 @@ if [ "$(id -u)" -ne 0 ]; then
|
||||||
SUDO="sudo"
|
SUDO="sudo"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
CURL_AUTH=()
|
||||||
|
if [ -n "$FORGE_USER" ] && [ -n "$FORGE_TOKEN" ]; then
|
||||||
|
forge_host="${FORGE_URL#*://}"
|
||||||
|
forge_host="${forge_host%%/*}"
|
||||||
|
$SUDO install -d -m 0755 "$APT_ETC/auth.conf.d"
|
||||||
|
printf 'machine %s\nlogin %s\npassword %s\n' \
|
||||||
|
"$forge_host" "$FORGE_USER" "$FORGE_TOKEN" \
|
||||||
|
| $SUDO tee "$AUTH" >/dev/null
|
||||||
|
$SUDO chmod 0600 "$AUTH"
|
||||||
|
CURL_AUTH=(--netrc-file "$AUTH")
|
||||||
|
fi
|
||||||
|
|
||||||
update_only_source() {
|
update_only_source() {
|
||||||
$SUDO apt-get update \
|
$SUDO apt-get update \
|
||||||
-o Dir::Etc::sourcelist="$1" \
|
-o Dir::Etc::sourcelist="$1" \
|
||||||
|
|
@ -81,7 +102,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"
|
||||||
curl -fsSL "$FORGE_URL/api/packages/$OWNER/debian/repository.key" | $SUDO tee "$KEYRING" >/dev/null
|
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
|
||||||
# tee inherits our umask; apt's unprivileged _apt user must be able to
|
# tee inherits our umask; apt's unprivileged _apt user must be able to
|
||||||
|
|
@ -94,7 +115,7 @@ $SUDO chmod 0644 "$KEYRING" "$LIST"
|
||||||
# is fatal; any other curl outcome (e.g. a network hiccup) is left for
|
# is fatal; any other curl outcome (e.g. a network hiccup) is left for
|
||||||
# apt-get update to report.
|
# apt-get update to report.
|
||||||
RELEASE_URL="$FORGE_URL/api/packages/$OWNER/debian/dists/$DISTRIBUTION/Release"
|
RELEASE_URL="$FORGE_URL/api/packages/$OWNER/debian/dists/$DISTRIBUTION/Release"
|
||||||
if [ "$(curl -sSL -o /dev/null -w '%{http_code}' "$RELEASE_URL" || true)" = "404" ]; then
|
if [ "$(curl "${CURL_AUTH[@]}" -sSL -o /dev/null -w '%{http_code}' "$RELEASE_URL" || true)" = "404" ]; then
|
||||||
echo "error: no stoke package has been published to the $OWNER Debian registry yet" >&2
|
echo "error: no stoke package has been published to the $OWNER Debian registry yet" >&2
|
||||||
echo "($RELEASE_URL returned 404)." >&2
|
echo "($RELEASE_URL returned 404)." >&2
|
||||||
echo "Install stoke via npm or manually instead — see the README." >&2
|
echo "Install stoke via npm or manually instead — see the README." >&2
|
||||||
|
|
|
||||||
|
|
@ -14,12 +14,13 @@ const SCRIPT = path.join(__dirname, '..', 'scripts', 'install-apt.sh');
|
||||||
// candAfterNodesource Candidate after an update once nodesource.list exists
|
// candAfterNodesource Candidate after an update once nodesource.list exists
|
||||||
// releaseStatus HTTP status curl reports for the registry Release file
|
// releaseStatus HTTP status curl reports for the registry Release file
|
||||||
// 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
|
||||||
// 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 }) {
|
function runScenario({ candInitial, candAfterUpdate, candAfterNodesource, preexistingNodesourceList, releaseStatus, sourceUpdateError, forgeUser, forgeToken }) {
|
||||||
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');
|
||||||
|
|
@ -92,6 +93,8 @@ function runScenario({ candInitial, candAfterUpdate, candAfterNodesource, preexi
|
||||||
CAND_AFTER_NODESOURCE: candAfterNodesource || '',
|
CAND_AFTER_NODESOURCE: candAfterNodesource || '',
|
||||||
RELEASE_STATUS: releaseStatus || '',
|
RELEASE_STATUS: releaseStatus || '',
|
||||||
SOURCE_UPDATE_ERROR: sourceUpdateError || '',
|
SOURCE_UPDATE_ERROR: sourceUpdateError || '',
|
||||||
|
FORGE_USER: forgeUser || '',
|
||||||
|
FORGE_TOKEN: forgeToken || '',
|
||||||
LC_ALL: 'es_ES.UTF-8', // localized environment; the script must force C
|
LC_ALL: 'es_ES.UTF-8', // localized environment; the script must force C
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
@ -107,6 +110,8 @@ function runScenario({ candInitial, candAfterUpdate, candAfterNodesource, preexi
|
||||||
nodesourceKeyMode: mode(path.join(aptEtc, 'keyrings', 'nodesource.asc')),
|
nodesourceKeyMode: mode(path.join(aptEtc, 'keyrings', 'nodesource.asc')),
|
||||||
forgeKeyMode: mode(path.join(aptEtc, 'keyrings', 'forgejo-heavy-duty.asc')),
|
forgeKeyMode: mode(path.join(aptEtc, 'keyrings', 'forgejo-heavy-duty.asc')),
|
||||||
forgeList: read(path.join(aptEtc, 'sources.list.d', 'forgejo-heavy-duty.list')),
|
forgeList: read(path.join(aptEtc, 'sources.list.d', 'forgejo-heavy-duty.list')),
|
||||||
|
forgeAuth: read(path.join(aptEtc, 'auth.conf.d', 'forgejo-heavy-duty.conf')),
|
||||||
|
forgeAuthMode: mode(path.join(aptEtc, 'auth.conf.d', 'forgejo-heavy-duty.conf')),
|
||||||
aptGetLog: read(path.join(state, 'apt-get.log')) || '',
|
aptGetLog: read(path.join(state, 'apt-get.log')) || '',
|
||||||
};
|
};
|
||||||
// Drop the throwaway tree after we have read everything we need.
|
// Drop the throwaway tree after we have read everything we need.
|
||||||
|
|
@ -205,3 +210,31 @@ test('network update failure stays fatal and never disables signature verificati
|
||||||
assert.doesNotMatch(s.forgeList, /trusted=yes/);
|
assert.doesNotMatch(s.forgeList, /trusted=yes/);
|
||||||
assert.doesNotMatch(s.aptGetLog, /install -y stoke/);
|
assert.doesNotMatch(s.aptGetLog, /install -y stoke/);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('private-registry credentials stay in a root-readable auth file, not the source URL', () => {
|
||||||
|
const s = runScenario({
|
||||||
|
candInitial: '22.23.1-1nodesource1',
|
||||||
|
forgeUser: 'apt-user',
|
||||||
|
forgeToken: 'secret-token',
|
||||||
|
});
|
||||||
|
assert.equal(s.res.status, 0, s.res.stderr);
|
||||||
|
assert.equal(s.forgeAuthMode, 0o600);
|
||||||
|
assert.equal(s.forgeAuth, [
|
||||||
|
'machine forgejo.heavyduty.builders',
|
||||||
|
'login apt-user',
|
||||||
|
'password secret-token',
|
||||||
|
'',
|
||||||
|
].join('\n'));
|
||||||
|
assert.doesNotMatch(s.forgeList, /apt-user|secret-token/);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('incomplete private-registry credentials fail before configuring apt', () => {
|
||||||
|
const s = runScenario({
|
||||||
|
candInitial: '22.23.1-1nodesource1',
|
||||||
|
forgeUser: 'apt-user',
|
||||||
|
});
|
||||||
|
assert.notEqual(s.res.status, 0);
|
||||||
|
assert.match(s.res.stderr, /FORGE_USER and FORGE_TOKEN must be set together/);
|
||||||
|
assert.equal(s.forgeList, null);
|
||||||
|
assert.equal(s.aptGetLog, '');
|
||||||
|
});
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue