Corp/ops/runner/install-runner-hardening.py
Игорь Надеин e3c677dda0
Some checks failed
Deploy production / deploy (push) Failing after 9m4s
Fix non-root runner SSH identity and verify image prerequisites
2026-09-10 17:06:11 +03:00

125 lines
4.7 KiB
Python

#!/usr/bin/env python3
"""Harden the existing runner without re-registering or changing its scope.
Before invoking this installer, pause assignment of new jobs in Gitea and wait
for any active job to finish. A ps snapshot only detects activity at one instant;
it cannot prevent the runner claiming another job before container replacement.
"""
import datetime
import json
import os
from pathlib import Path
import shutil
import subprocess
import time
RUNNER_IMAGE = "robotail/gitea-light-runner:0.2.13-hardened"
def run(*args):
return subprocess.run(args, check=True, capture_output=True, text=True).stdout
def prepare_image(source):
# Reuse the installed base image; no dependency download or package upgrade.
run(
"docker", "build", "--pull=false", "--network=none",
"--tag", RUNNER_IMAGE, "--file", str(source / "Dockerfile"), str(source),
)
# Exercise the real OpenSSH client without a key, registration or network.
# Merely observing a healthy runner daemon does not test job prerequisites.
run(
"docker", "run", "--rm", "--network", "none", "--read-only",
"--user", "10001:10001", "--entrypoint", "/bin/sh", RUNNER_IMAGE,
"-ec", 'test "$(id -un)" = runner; ssh -G -F /dev/null example.invalid >/dev/null',
)
def main():
if os.geteuid() != 0:
raise SystemExit("Run with sudo on Gitea after confirming the runner is idle.")
source = Path(__file__).resolve().parent
target = Path("/opt/gitea-light-runner")
registration = target / "data/.runner"
if target.is_symlink() or registration.is_symlink() or not registration.is_file():
raise SystemExit("Unexpected runner paths; refusing to modify them.")
for name in ("compose.yaml", "config.yaml", "Dockerfile"):
if (target / name).is_symlink():
raise SystemExit("Unexpected configuration symlink.")
# The bootstrap file is not used: registration and global scope stay intact.
process_names = run(
"docker", "exec", "gitea-light-runner", "ps", "-eo", "comm"
).splitlines()[1:]
if any(name.strip() not in {"tini", "act_runner", "ps"} for name in process_names):
raise SystemExit("Runner may be executing a job; retry when idle.")
run("docker", "compose", "-f", str(source / "compose.yaml"), "config", "--quiet")
prepare_image(source)
stamp = datetime.datetime.now(datetime.timezone.utc).strftime("%Y%m%dT%H%M%SZ")
backup = target / f"hardening-backup-{stamp}"
backup.mkdir(mode=0o700)
existing = {}
for name in ("compose.yaml", "config.yaml", "Dockerfile"):
existing[name] = (target / name).exists()
if existing[name]:
shutil.copy2(target / name, backup / name)
old_stat = registration.stat()
try:
for name in ("compose.yaml", "config.yaml", "Dockerfile"):
shutil.copyfile(source / name, target / name)
os.chown(target / name, 0, 0)
os.chmod(target / name, 0o644)
os.chown(registration, 10001, 10001)
os.chmod(registration, 0o400)
run(
"docker",
"compose",
"--project-directory",
str(target),
"up",
"-d",
"--no-build",
)
time.sleep(5)
state = json.loads(run("docker", "inspect", "gitea-light-runner"))[0]
if not state["State"]["Running"] or state["RestartCount"]:
raise RuntimeError("Runner failed to stay running.")
# Capture log without exposing registration credentials or old task output.
log = subprocess.run(
["docker", "logs", "--since", "20s", "gitea-light-runner"],
capture_output=True,
text=True,
check=True,
)
if "declare successfully" not in log.stdout + log.stderr:
raise RuntimeError("Runner registration acknowledgement not observed.")
except BaseException:
for name, was_present in existing.items():
if was_present:
shutil.copy2(backup / name, target / name)
else:
(target / name).unlink(missing_ok=True)
os.chown(registration, old_stat.st_uid, old_stat.st_gid)
os.chmod(registration, old_stat.st_mode & 0o777)
run(
"docker",
"compose",
"--project-directory",
str(target),
"up",
"-d",
"--no-build",
)
raise
print(
"Existing global runner acknowledged by Gitea; scope and registration unchanged."
)
print(
"Non-root, read-only root filesystem, read-only registration, bounded temporary workspace."
)
print(f"Configuration backup: {backup}")
if __name__ == "__main__":
main()