mirror of
https://github.com/rcourtman/Pulse.git
synced 2026-10-13 00:34:43 +00:00
Bind action runner to enrolled agent identity
This commit is contained in:
parent
6ee3b7e24e
commit
77afff4f60
15 changed files with 254 additions and 25 deletions
|
|
@ -963,6 +963,7 @@ read_action_runner_env_value() {
|
|||
revoke_action_runner_credential() {
|
||||
local runner_url=""
|
||||
local runner_hostname=""
|
||||
local runner_agent_id_direct=""
|
||||
local runner_agent_id_file=""
|
||||
local runner_agent_id=""
|
||||
local runner_token_file=""
|
||||
|
|
@ -975,17 +976,22 @@ revoke_action_runner_credential() {
|
|||
|
||||
runner_url=$(read_action_runner_env_value "PULSE_URL" || true)
|
||||
runner_hostname=$(read_action_runner_env_value "PULSE_AGENT_RUNNER_HOSTNAME" || true)
|
||||
runner_agent_id_direct=$(read_action_runner_env_value "PULSE_AGENT_RUNNER_AGENT_ID" || true)
|
||||
runner_agent_id_file=$(read_action_runner_env_value "PULSE_AGENT_RUNNER_AGENT_ID_FILE" || true)
|
||||
runner_token_file=$(read_action_runner_env_value "PULSE_AGENT_RUNNER_TOKEN_FILE" || true)
|
||||
[[ "$runner_url" =~ ^https?://[^[:space:]]+$ && -n "$runner_hostname" ]] || return 1
|
||||
[[ "$runner_agent_id_file" == /* && "$runner_agent_id_file" != *'/../'* &&
|
||||
-f "$runner_agent_id_file" && ! -L "$runner_agent_id_file" ]] || return 1
|
||||
[[ "$runner_token_file" == /* && "$runner_token_file" != *'/../'* &&
|
||||
-f "$runner_token_file" && ! -L "$runner_token_file" ]] || return 1
|
||||
runner_agent_id=$(head -1 "$runner_agent_id_file" 2>/dev/null || true)
|
||||
(( ${#runner_agent_id} >= 1 && ${#runner_agent_id} <= 256 &&
|
||||
if [[ -n "$runner_agent_id_direct" ]]; then
|
||||
runner_agent_id="$runner_agent_id_direct"
|
||||
else
|
||||
[[ "$runner_agent_id_file" == /* && "$runner_agent_id_file" != *'/../'* &&
|
||||
-f "$runner_agent_id_file" && ! -L "$runner_agent_id_file" ]] || return 1
|
||||
runner_agent_id=$(head -1 "$runner_agent_id_file" 2>/dev/null || true)
|
||||
fi
|
||||
(( ${#runner_agent_id} >= 1 && ${#runner_agent_id} <= 128 &&
|
||||
${#runner_hostname} >= 1 && ${#runner_hostname} <= 253 )) || return 1
|
||||
[[ "$runner_agent_id" =~ ^[A-Za-z0-9._:-]+$ &&
|
||||
[[ "$runner_agent_id" =~ ^[A-Za-z0-9][A-Za-z0-9._:-]*$ &&
|
||||
"$runner_hostname" =~ ^[A-Za-z0-9._:-]+$ ]] || return 1
|
||||
token_mode=$(stat -c '%a' "$runner_token_file" 2>/dev/null || true)
|
||||
token_size=$(wc -c < "$runner_token_file" 2>/dev/null | tr -d ' ' || true)
|
||||
|
|
@ -1368,6 +1374,11 @@ write_action_runner_config() {
|
|||
write_action_runner_env_value "PULSE_AGENT_RUNNER_STATE_DIR" "$ACTION_RUNNER_STATE_DIR"
|
||||
write_action_runner_env_value "PULSE_AGENT_RUNNER_HEALTH_FILE" "$ACTION_RUNNER_HEALTH_FILE"
|
||||
write_action_runner_env_value "PULSE_AGENT_RUNNER_ACTIVATION_NONCE" "$ACTION_RUNNER_ACTIVATION_NONCE"
|
||||
if [[ -n "$AGENT_ID" ]]; then
|
||||
[[ ${#AGENT_ID} -le 128 && "$AGENT_ID" =~ ^[A-Za-z0-9][A-Za-z0-9._:-]*$ ]] ||
|
||||
fail "Action runner requires a valid canonical agent identity" "$EXIT_MISSING_ARGS"
|
||||
write_action_runner_env_value "PULSE_AGENT_RUNNER_AGENT_ID" "$AGENT_ID"
|
||||
fi
|
||||
write_action_runner_env_value "PULSE_AGENT_RUNNER_AGENT_ID_FILE" "${STATE_DIR%/}/agent-id"
|
||||
write_action_runner_env_value "PULSE_AGENT_RUNNER_HOSTNAME" "$runner_hostname"
|
||||
if [[ -n "$SERVER_FINGERPRINT" ]]; then
|
||||
|
|
@ -1420,6 +1431,15 @@ write_action_runner_env_value() {
|
|||
printf '%s="%s"\n' "$key" "$value" >> "$ACTION_RUNNER_ENV_FILE"
|
||||
}
|
||||
|
||||
resolve_action_runner_agent_id() {
|
||||
local agent_id="${AGENT_ID:-}"
|
||||
if [[ -z "$agent_id" && -s "${STATE_DIR%/}/agent-id" && ! -L "${STATE_DIR%/}/agent-id" ]]; then
|
||||
agent_id=$(head -1 "${STATE_DIR%/}/agent-id" 2>/dev/null || true)
|
||||
fi
|
||||
[[ ${#agent_id} -ge 1 && ${#agent_id} -le 128 && "$agent_id" =~ ^[A-Za-z0-9][A-Za-z0-9._:-]*$ ]] || return 1
|
||||
printf '%s\n' "$agent_id"
|
||||
}
|
||||
|
||||
provision_action_runner() {
|
||||
local backup_suffix=".pulse-install-backup.$$"
|
||||
local path=""
|
||||
|
|
@ -1473,10 +1493,8 @@ provision_action_runner() {
|
|||
if [[ "$apply_succeeded" == "true" ]]; then
|
||||
local attempt=0
|
||||
while [[ "$attempt" -lt 30 ]]; do
|
||||
local expected_agent_id="${AGENT_ID}"
|
||||
if [[ -s "${STATE_DIR%/}/agent-id" ]]; then
|
||||
expected_agent_id=$(head -1 "${STATE_DIR%/}/agent-id" 2>/dev/null || true)
|
||||
fi
|
||||
local expected_agent_id=""
|
||||
expected_agent_id=$(resolve_action_runner_agent_id || true)
|
||||
if systemctl is-active --quiet "${ACTION_RUNNER_NAME}.service" &&
|
||||
action_runner_health_matches_activation "$expected_agent_id" "$activation_nonce"; then
|
||||
runner_active="true"
|
||||
|
|
|
|||
|
|
@ -5952,6 +5952,7 @@ func TestInstallSHActionRunnerIsSeparateOptInLifecycle(t *testing.T) {
|
|||
`The action runner must use a separate credential from the collector token`,
|
||||
`Preserving existing separately enabled action-runner profile`,
|
||||
`write_action_runner_env_value "PULSE_AGENT_RUNNER_TOKEN_FILE" "$ACTION_RUNNER_TOKEN_FILE"`,
|
||||
`write_action_runner_env_value "PULSE_AGENT_RUNNER_AGENT_ID" "$AGENT_ID"`,
|
||||
`write_action_runner_env_value "PULSE_AGENT_RUNNER_AGENT_ID_FILE" "${STATE_DIR%/}/agent-id"`,
|
||||
`write_action_runner_env_value "PULSE_AGENT_RUNNER_HEALTH_FILE" "$ACTION_RUNNER_HEALTH_FILE"`,
|
||||
`write_action_runner_env_value "PULSE_AGENT_RUNNER_ACTIVATION_NONCE" "$ACTION_RUNNER_ACTIVATION_NONCE"`,
|
||||
|
|
@ -5979,6 +5980,66 @@ func TestInstallSHActionRunnerIsSeparateOptInLifecycle(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
func TestInstallSHActionRunnerConfigBindsKnownAgentIDImmediately(t *testing.T) {
|
||||
root := t.TempDir()
|
||||
envFile := filepath.Join(root, "runner.env")
|
||||
script := `
|
||||
set -euo pipefail
|
||||
ACTION_RUNNER_CONFIG_DIR="` + filepath.Join(root, "config") + `"
|
||||
ACTION_RUNNER_STATE_DIR="` + filepath.Join(root, "runner-state") + `"
|
||||
ACTION_RUNNER_TOKEN_FILE="` + filepath.Join(root, "config", "token") + `"
|
||||
ACTION_RUNNER_ENV_FILE="` + envFile + `"
|
||||
ACTION_RUNNER_HEALTH_FILE="` + filepath.Join(root, "runner-state", "health.json") + `"
|
||||
ACTION_RUNNER_ACTIVATION_NONCE="` + strings.Repeat("a", 64) + `"
|
||||
STATE_DIR="` + filepath.Join(root, "collector-state") + `"
|
||||
HOSTNAME_OVERRIDE="secure-runtime.lab"
|
||||
AGENT_ID="agent-bound-before-collector-report"
|
||||
ACTION_TOKEN="runner-secret"
|
||||
PULSE_URL="https://pulse.example"
|
||||
SERVER_FINGERPRINT=""
|
||||
CURL_CA_BUNDLE=""
|
||||
INSECURE="false"
|
||||
EXIT_GENERAL=1
|
||||
EXIT_MISSING_ARGS=2
|
||||
fail() { printf '%s\n' "$1" >&2; exit "$2"; }
|
||||
install() { local destination="${!#}"; mkdir -p "$destination"; chmod 0700 "$destination"; }
|
||||
chown() { return 0; }
|
||||
` + extractInstallShellFunction(t, "write_action_runner_env_value") + `
|
||||
` + extractInstallShellFunction(t, "write_action_runner_config") + `
|
||||
write_action_runner_config
|
||||
`
|
||||
if out, err := exec.Command("bash", "-c", script).CombinedOutput(); err != nil {
|
||||
t.Fatalf("write action-runner config: %v\n%s", err, out)
|
||||
}
|
||||
content, err := os.ReadFile(envFile)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
want := `PULSE_AGENT_RUNNER_AGENT_ID="agent-bound-before-collector-report"`
|
||||
if !strings.Contains(string(content), want) {
|
||||
t.Fatalf("action-runner environment missing immediate identity binding %q:\n%s", want, content)
|
||||
}
|
||||
}
|
||||
|
||||
func TestInstallSHActionRunnerDirectIdentityOverridesStaleCollectorFile(t *testing.T) {
|
||||
stateDir := t.TempDir()
|
||||
mustWrite(t, filepath.Join(stateDir, "agent-id"), "stale-agent-id\n")
|
||||
script := `
|
||||
set -euo pipefail
|
||||
STATE_DIR="` + stateDir + `"
|
||||
AGENT_ID="current-agent-id"
|
||||
` + extractInstallShellFunction(t, "resolve_action_runner_agent_id") + `
|
||||
resolve_action_runner_agent_id
|
||||
`
|
||||
out, err := exec.Command("bash", "-c", script).CombinedOutput()
|
||||
if err != nil {
|
||||
t.Fatalf("resolve action-runner identity: %v\n%s", err, out)
|
||||
}
|
||||
if got := strings.TrimSpace(string(out)); got != "current-agent-id" {
|
||||
t.Fatalf("resolved action-runner identity = %q, want direct binding", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestInstallSHActionRunnerReadinessRequiresCurrentActivationNonce(t *testing.T) {
|
||||
root := t.TempDir()
|
||||
healthPath := filepath.Join(root, "health.json")
|
||||
|
|
@ -6055,16 +6116,15 @@ func TestInstallSHActionRunnerSelfRevokeUsesPrivateCredential(t *testing.T) {
|
|||
root := t.TempDir()
|
||||
envFile := filepath.Join(root, "runner.env")
|
||||
tokenFile := filepath.Join(root, "token")
|
||||
agentIDFile := filepath.Join(root, "agent-id")
|
||||
mustWrite(t, tokenFile, token+"\n")
|
||||
mustWrite(t, agentIDFile, "agent-secure-runtime\n")
|
||||
if err := os.Chmod(tokenFile, 0o600); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
mustWrite(t, envFile, strings.Join([]string{
|
||||
`PULSE_URL="` + server.URL + `"`,
|
||||
`PULSE_AGENT_RUNNER_HOSTNAME="secure-runtime.lab"`,
|
||||
`PULSE_AGENT_RUNNER_AGENT_ID_FILE="` + agentIDFile + `"`,
|
||||
`PULSE_AGENT_RUNNER_AGENT_ID="agent-secure-runtime"`,
|
||||
`PULSE_AGENT_RUNNER_AGENT_ID_FILE="` + filepath.Join(root, "missing-agent-id") + `"`,
|
||||
`PULSE_AGENT_RUNNER_TOKEN_FILE="` + tokenFile + `"`,
|
||||
}, "\n")+"\n")
|
||||
|
||||
|
|
|
|||
|
|
@ -255,6 +255,7 @@ class CanonicalCompletionGuardTest(unittest.TestCase):
|
|||
self.assertEqual(
|
||||
policy_ids,
|
||||
[
|
||||
"service-discovery-assurance-runtime",
|
||||
"maintenance-verification-runtime",
|
||||
"availability-certificate-runtime",
|
||||
"discovery-provider-runtime",
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue