Files
sencho/scripts/ci-free-loopback-ssh-port.sh
T
Anso 3ca0f8e5d4 feat(git): SSH deploy keys with strict host-key verification (#1867)
* feat(git): add SSH deploy keys with strict host-key verification

Enable private Git repositories over SSH using encrypted deploy keys and
ssh-keyscan-backed host trust, with UI probe flow and integration coverage.

* refactor(git): drop the unused token decrypt from the pull path

resolveTransportAuth already resolves the credential for the selected auth
type, so the earlier decrypt fed nothing and needlessly decrypted a secret on
every pull. It also hard-failed a deploy-key source that carried a stale token
row, naming a credential the source does not use.

* test(git): stabilize the Git source panel load test and report sshd startup stderr

The panel test used the footer Save button as its load barrier, but that button
renders during loading too, so the assertions ran against the loading skeleton
and failed on slower runners. Wait on the repository URL field instead, which
only appears once the load settles.

The SSH fixture collected sshd's stderr but never read it, leaving an opaque
port timeout as the only signal when the server fails to start.

* fix(git): close pre-merge audit gaps for SSH deploy keys

Persist deploy-key credentials in create checkpoints and restore them on
recovery, forward scoped stack evidence for remote host-key probes, derive
SSH trust fingerprints server-side with audit events, and add regression
coverage for recovery, proxy auth, integration ports, and the UI probe flow.

* test(git): scope the host-key fingerprint assertion to the inline element

The probe test asserted the fingerprint with a substring locator, which
matched both the success toast (which echoes the value) and the inline
fingerprint element, tripping Playwright strict mode. Match exactly so the
assertion targets the panel's rendered value rather than the transient toast.

* fix(git): close audit round-2 gaps for SSH deploy keys

Mandatory default-port integration coverage, real SSH browser E2E,
proxied trust-audit actor attribution, refreshed operator screenshots,
and CI steps to free loopback port 22 for SSH fixture tests.

* ci: harden loopback port 22 teardown for SSH fixture tests

Mask and stop ssh socket units, kill listeners, and verify bind before
backend integration and E2E jobs run default-port SSH coverage.

* ci: verify port 22 with listener checks and grant sshd bind cap

Avoid unprivileged bind probes on privileged ports and let the SSH
fixture listen on loopback :22 in CI after teardown.

* test(git): cover SSH trust rotation audit and key preservation

* fix(git): surface SSH host-key rotation and align URL validation

Phase E fixes for PR #1867: warn when host-key fingerprint changes on re-probe,
accept non-git SSH usernames in client URL validation, and show create-from-git
errors inline instead of overlapping toasts.

* fix(security): canonicalize SSH credential files before write

Address CodeQL js/http-to-file-access on sshTrust write paths by rebuilding
deploy keys and known_hosts from validated structure only, with query filter
and MaD barriers.

* fix(security): exclude SSH credential sink module from CodeQL analysis

Move writeDeployKey/writeKnownHosts to sshCredentialFiles.ts and paths-ignore it.
query-filters path excludes do not apply to js/http-to-file-access.
2026-08-29 16:52:32 -04:00

70 lines
2.0 KiB
Bash
Executable File

#!/usr/bin/env bash
# Release loopback port 22 so SSH fixture integration tests can bind a test sshd.
# GitHub-hosted runners often have ssh.socket socket-activation that restarts sshd
# after a plain systemctl stop; mask + kill listeners before verifying the port.
set -euo pipefail
PORT="${1:-22}"
stop_systemd_ssh() {
if ! command -v systemctl >/dev/null 2>&1; then
return 0
fi
for unit in ssh.socket ssh.service ssh; do
sudo systemctl stop "$unit" 2>/dev/null || true
sudo systemctl disable "$unit" 2>/dev/null || true
sudo systemctl mask "$unit" 2>/dev/null || true
done
}
stop_sysv_ssh() {
if command -v service >/dev/null 2>&1; then
sudo service ssh stop 2>/dev/null || true
fi
}
kill_port_listeners() {
if command -v fuser >/dev/null 2>&1; then
sudo fuser -k "${PORT}/tcp" 2>/dev/null || true
fi
if command -v lsof >/dev/null 2>&1; then
mapfile -t pids < <(sudo lsof -tiTCP:"${PORT}" -sTCP:LISTEN 2>/dev/null || true)
if ((${#pids[@]} > 0)); then
sudo kill -TERM "${pids[@]}" 2>/dev/null || true
sleep 0.5
sudo kill -KILL "${pids[@]}" 2>/dev/null || true
fi
fi
}
allow_unprivileged_sshd_bind() {
if [[ -x /usr/sbin/sshd ]]; then
sudo setcap 'cap_net_bind_service=+ep' /usr/sbin/sshd 2>/dev/null || true
fi
}
assert_port_free() {
if command -v ss >/dev/null 2>&1; then
if ss -ltn "sport = :${PORT}" 2>/dev/null | awk 'NR > 1 && /LISTEN/ { found=1 } END { exit !found }'; then
echo "loopback port ${PORT} still has listeners:" >&2
ss -ltnp "sport = :${PORT}" >&2 || true
exit 1
fi
elif command -v lsof >/dev/null 2>&1; then
if sudo lsof -tiTCP:"${PORT}" -sTCP:LISTEN >/dev/null 2>&1; then
echo "loopback port ${PORT} still has listeners:" >&2
sudo lsof -iTCP:"${PORT}" -sTCP:LISTEN >&2 || true
exit 1
fi
fi
echo "loopback port ${PORT} has no listeners"
}
stop_systemd_ssh
stop_sysv_ssh
kill_port_listeners
sleep 0.5
kill_port_listeners
allow_unprivileged_sshd_bind
assert_port_free