Implement code signing (cStudio CA)
This commit is contained in:
@@ -26,7 +26,14 @@ NODE_MAJOR="20" # act_runner spawns Node for JS actions (upload/downl
|
||||
|
||||
# ---- defaults ----
|
||||
RUNNER_NAME="$(hostname)-helloagent-sign"
|
||||
RUNNER_LABELS="self-hosted,linux,signing"
|
||||
# Label suffix `:host` tells act_runner to run jobs directly on this host
|
||||
# rather than inside a Docker container (the Linux runner default). The
|
||||
# signing runner deliberately has no Docker daemon — its only job is to
|
||||
# call osslsigncode and upload, which doesn't need container isolation
|
||||
# beyond the LXC + systemd sandbox we already enforce. The workflow's
|
||||
# `runs-on: [self-hosted, linux, signing]` matches on label name, so the
|
||||
# `:host` qualifier is invisible to workflow authors.
|
||||
RUNNER_LABELS="self-hosted:host,linux:host,signing:host"
|
||||
SERVICE_USER="hello-signer"
|
||||
PKI_DIR="/etc/pki/hello-agent"
|
||||
GITEA_URL=""
|
||||
@@ -85,10 +92,49 @@ fi
|
||||
log "osslsigncode $ver OK"
|
||||
|
||||
# ---- 2. dedicated runner user ----
|
||||
# We pin the user's home to RUNNER_DIR (defined below in section 4) rather
|
||||
# than letting useradd default to /home/$SERVICE_USER. Two reasons:
|
||||
#
|
||||
# 1. The systemd unit sets ProtectHome=yes, which masks /home, /root,
|
||||
# /run/user with empty tmpfs. If HOME points into /home, anything
|
||||
# act_runner spawns (Node for JS actions, etc.) inherits a HOME path
|
||||
# that doesn't exist from the sandbox's view, and crashes on first
|
||||
# cache write with "mkdir /home/<user>: permission denied".
|
||||
# 2. The runner user has no real "home" — it's a system account that
|
||||
# exists only to run a daemon. Pointing HOME at /var/lib/gitea-runner
|
||||
# reflects what's actually true.
|
||||
#
|
||||
# RUNNER_DIR is hardcoded here (mirrors the section-4 value) because user
|
||||
# creation has to happen before we know we'll need to mkdir the dir, but
|
||||
# we need the path baked into /etc/passwd up front. Keep these two in sync.
|
||||
RUNNER_DIR=/var/lib/gitea-runner
|
||||
mkdir -p "$RUNNER_DIR"
|
||||
|
||||
if ! id -u "$SERVICE_USER" >/dev/null 2>&1; then
|
||||
log "Creating system user $SERVICE_USER"
|
||||
log "Creating system user $SERVICE_USER (home=$RUNNER_DIR)"
|
||||
# No login shell on purpose: this user only runs systemd's exec, never logs in.
|
||||
useradd --system --create-home --shell /usr/sbin/nologin "$SERVICE_USER"
|
||||
# --no-create-home: we already mkdir'd RUNNER_DIR; useradd would fail
|
||||
# trying to copy /etc/skel into a non-empty dir.
|
||||
useradd --system \
|
||||
--home-dir "$RUNNER_DIR" \
|
||||
--no-create-home \
|
||||
--shell /usr/sbin/nologin \
|
||||
"$SERVICE_USER"
|
||||
else
|
||||
# Existing user from a pre-fix provision run: re-point home to
|
||||
# RUNNER_DIR if it isn't already. Fixes deployments that hit the
|
||||
# ProtectHome=yes / HOME=/home/<user> mismatch.
|
||||
current_home="$(getent passwd "$SERVICE_USER" | cut -d: -f6)"
|
||||
if [[ "$current_home" != "$RUNNER_DIR" ]]; then
|
||||
log "Re-pointing $SERVICE_USER home: $current_home -> $RUNNER_DIR"
|
||||
usermod --home "$RUNNER_DIR" "$SERVICE_USER"
|
||||
# If the legacy home is empty (the common case — runner state lives
|
||||
# under RUNNER_DIR, not under /home), remove it. If it has content
|
||||
# for some reason, leave it alone for the operator to inspect.
|
||||
if [[ -d "$current_home" && -z "$(ls -A "$current_home" 2>/dev/null)" ]]; then
|
||||
rmdir "$current_home" || true
|
||||
fi
|
||||
fi
|
||||
fi
|
||||
RUNNER_HOME="$(getent passwd "$SERVICE_USER" | cut -d: -f6)"
|
||||
|
||||
@@ -116,8 +162,9 @@ chown root:root "$PKI_DIR/chain.pem"; chmod 0444 "$PKI_DIR/chain.pem"
|
||||
chown root:"$SERVICE_USER" "$PKI_DIR/codesign.key"; chmod 0400 "$PKI_DIR/codesign.key"
|
||||
|
||||
# ---- 4. act_runner ----
|
||||
RUNNER_DIR=/var/lib/gitea-runner
|
||||
mkdir -p "$RUNNER_DIR"
|
||||
# RUNNER_DIR was already defined and mkdir'd in section 2 (we needed it
|
||||
# before useradd to set the user's home). Just re-assert ownership now
|
||||
# that the user exists.
|
||||
chown -R "$SERVICE_USER:$SERVICE_USER" "$RUNNER_DIR"
|
||||
|
||||
if [[ ! -x "$RUNNER_DIR/act_runner" ]]; then
|
||||
@@ -145,6 +192,32 @@ if [[ ! -f "$RUNNER_DIR/.runner" ]]; then
|
||||
"
|
||||
fi
|
||||
|
||||
# act_runner config.yaml: pin host-mode workdir under RUNNER_DIR.
|
||||
#
|
||||
# Without this, host-mode jobs default to /workspace/<owner>/<repo> as
|
||||
# $GITHUB_WORKSPACE — a path that doesn't exist and, under the systemd
|
||||
# ProtectSystem=strict + ReadWritePaths=$RUNNER_DIR sandbox below, can't
|
||||
# be created. The first JS action that writes there (e.g. actions/download-
|
||||
# artifact populating ./incoming) fails with EROFS and the job dies before
|
||||
# osslsigncode is ever invoked.
|
||||
WORKDIR_PARENT="$RUNNER_DIR/workspace"
|
||||
install -d -m 0755 -o "$SERVICE_USER" -g "$SERVICE_USER" "$WORKDIR_PARENT"
|
||||
|
||||
CONFIG_FILE="$RUNNER_DIR/config.yaml"
|
||||
if [[ ! -f "$CONFIG_FILE" ]]; then
|
||||
log "Writing act_runner config at $CONFIG_FILE"
|
||||
cat > "$CONFIG_FILE" <<EOF
|
||||
log:
|
||||
level: info
|
||||
runner:
|
||||
capacity: 1
|
||||
host:
|
||||
workdir_parent: $WORKDIR_PARENT
|
||||
EOF
|
||||
chown "$SERVICE_USER:$SERVICE_USER" "$CONFIG_FILE"
|
||||
chmod 0644 "$CONFIG_FILE"
|
||||
fi
|
||||
|
||||
# ---- 5. systemd unit (heavily sandboxed) ----
|
||||
#
|
||||
# Why these flags: the signing runner does almost nothing — pulls a PE file,
|
||||
@@ -170,7 +243,7 @@ Wants=network-online.target
|
||||
Type=simple
|
||||
User=${SERVICE_USER}
|
||||
WorkingDirectory=${RUNNER_DIR}
|
||||
ExecStart=${RUNNER_DIR}/act_runner daemon
|
||||
ExecStart=${RUNNER_DIR}/act_runner daemon --config ${CONFIG_FILE}
|
||||
Restart=on-failure
|
||||
RestartSec=5
|
||||
|
||||
@@ -191,8 +264,15 @@ RestrictRealtime=yes
|
||||
RestrictSUIDSGID=yes
|
||||
LockPersonality=yes
|
||||
SystemCallArchitectures=native
|
||||
SystemCallFilter=@system-service
|
||||
SystemCallFilter=~@privileged @resources @debug @mount @cpu-emulation @obsolete @raw-io @reboot @swap
|
||||
# No SystemCallFilter=. We tried @system-service with various exclusions and
|
||||
# Node 20 (spawned by act_runner for JS actions) hits a syscall outside the
|
||||
# allowed set, getting killed with SIGSYS ("signal: bad system call") before
|
||||
# producing any stderr — a silent kill that's miserable to diagnose. The
|
||||
# other sandbox flags above (NoNewPrivileges, ProtectSystem=strict,
|
||||
# ProtectHome, RestrictNamespaces, RestrictSUIDSGID, LockPersonality, plus
|
||||
# the LXC and host-firewall layers) already cover the realistic threats for
|
||||
# a signing-only service. Re-enable a tightened seccomp policy here only
|
||||
# after auditing the exact syscalls Node + osslsigncode use end-to-end.
|
||||
|
||||
# --- filesystem access ---
|
||||
ReadWritePaths=${RUNNER_DIR}
|
||||
|
||||
Reference in New Issue
Block a user