harness: read Syncro keys from vault, stop hardcoding the repo root
Two defects found while running /wiki-compile, both from tooling assuming
a path instead of resolving it.
1. Plaintext Syncro API keys. Mike's and Howard's live PSA keys were
copy-pasted into three command files, a script, and two catalog docs --
despite both already being vaulted at msp-tools/syncro and
msp-tools/syncro-howard. Replaced with reads from the SOPS vault via a
new sourced helper, .claude/scripts/syncro-env.sh. Write paths fail
closed; read-only paths degrade to skipped enrichment rather than a
wrong key. Per-user mapping is unchanged, so Syncro attribution is too.
2. Hardcoded repo root. wiki-compile/wiki-lint/inject-standards and
gen_b64.py hardcoded D:/claudetools; this machine is C:/claudetools.
syncro.md also read ~/.claude/identity.json before the repo copy -- the
same bug that made remediation-tool's consent-audit report a fully
consented tenant as RED. Root now resolves from the script's own
location, with identity.json claudetools_root as the override.
get-identity.sh had a chicken-and-egg bug: it read ${CLAUDETOOLS_ROOT:-.}
but never set it, so every caller had to already know the root. It now
self-resolves and exports CLAUDETOOLS_ROOT + VAULT_ROOT.
Verified: all three command setup blocks authenticate against live Syncro;
get-identity.sh works from any cwd and honors a pre-set root; gps-rmm
autoenroll resolves its key from the vault. security-review: no findings.
NOTE: both keys remain valid in git history. Rotation in the Syncro portal
is the required follow-up -- this commit does not resolve that exposure.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -1,5 +1,5 @@
|
||||
"""Generate base64 of the WatchdogAlertsSection component."""
|
||||
import base64, sys
|
||||
import base64, os, sys
|
||||
|
||||
BT = chr(96)
|
||||
BULLET = "\xb7"
|
||||
@@ -195,7 +195,9 @@ lines = [
|
||||
component = "".join(lines)
|
||||
b64 = base64.b64encode(component.encode("utf-8")).decode("ascii")
|
||||
|
||||
with open("D:/claudetools/.claude/scripts/component.b64", "w") as f:
|
||||
# Write next to this script — the repo root is machine-specific, never hardcode it.
|
||||
out_path = os.path.join(os.path.dirname(os.path.abspath(__file__)), "component.b64")
|
||||
with open(out_path, "w") as f:
|
||||
f.write(b64)
|
||||
|
||||
print(f"Component: {len(component)} chars")
|
||||
|
||||
@@ -12,7 +12,14 @@
|
||||
# (but does NOT exit, so the caller continues). This ensures skills never break on
|
||||
# missing identity - they just attribute to "Unknown".
|
||||
|
||||
IDENTITY_FILE="${CLAUDETOOLS_ROOT:-.}/.claude/identity.json"
|
||||
# Resolve the repo root from this script's own location (mirrors vault.sh) so callers
|
||||
# don't have to know it. An already-set CLAUDETOOLS_ROOT still wins, and identity.json's
|
||||
# claudetools_root overrides both. Never hardcode a drive letter.
|
||||
if [ -z "${CLAUDETOOLS_ROOT:-}" ]; then
|
||||
export CLAUDETOOLS_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
|
||||
fi
|
||||
|
||||
IDENTITY_FILE="$CLAUDETOOLS_ROOT/.claude/identity.json"
|
||||
|
||||
if [ ! -f "$IDENTITY_FILE" ]; then
|
||||
echo "[WARNING] identity.json not found at $IDENTITY_FILE - attribution will be 'Unknown'" >&2
|
||||
@@ -28,5 +35,11 @@ export USER_SHORT=$(jq -r '.user // "unknown"' "$IDENTITY_FILE" 2>/dev/null || e
|
||||
export MACHINE=$(jq -r '.machine // "unknown"' "$IDENTITY_FILE" 2>/dev/null || echo "unknown")
|
||||
export USER_EMAIL=$(jq -r '.email // "unknown@unknown.com"' "$IDENTITY_FILE" 2>/dev/null || echo "unknown@unknown.com")
|
||||
|
||||
# identity.json is authoritative for the root and the vault location.
|
||||
_root_override=$(jq -r '.claudetools_root // empty' "$IDENTITY_FILE" 2>/dev/null)
|
||||
[ -n "$_root_override" ] && [ -d "$_root_override" ] && export CLAUDETOOLS_ROOT="$_root_override"
|
||||
export VAULT_ROOT=$(jq -r '.vault_path // empty' "$IDENTITY_FILE" 2>/dev/null)
|
||||
unset _root_override
|
||||
|
||||
# Success
|
||||
return 0
|
||||
|
||||
@@ -13,7 +13,8 @@ TS="$(date '+%Y-%m-%d %H:%M')"
|
||||
eval "$(bash .claude/scripts/rmm-auth.sh 2>/dev/null)" >/dev/null 2>&1
|
||||
if [ -z "${TOKEN:-}" ]; then echo "$TS auth FAILED" >> "$LOG"; exit 0; fi
|
||||
SEC="$(bash .claude/scripts/vault.sh get-field msp-tools/screenconnect.sops.yaml credentials.api_secret 2>/dev/null | tr -d '\r\n')"
|
||||
SK="Tde5174a6e9e312d14-02fd5bfe0f0ee40c87d027507c680e18"
|
||||
SK="$(bash .claude/scripts/vault.sh get-field msp-tools/syncro-howard credentials.credential 2>/dev/null | tr -d '\r\n')"
|
||||
if [ -z "$SK" ]; then echo "$TS syncro key read FAILED" >> "$LOG"; exit 0; fi
|
||||
|
||||
OUT="$(RMM="$RMM" TOK="$TOKEN" SK="$SK" SC_SECRET="$SEC" python projects/gps-rmm-audit/tools/rebuild-and-push.py 2>/dev/null | tail -2)"
|
||||
PUSHED="$(echo "$OUT" | grep -oE 'pushed: [0-9]+' | grep -oE '[0-9]+' | head -1 || true)"
|
||||
|
||||
57
.claude/scripts/syncro-env.sh
Normal file
57
.claude/scripts/syncro-env.sh
Normal file
@@ -0,0 +1,57 @@
|
||||
#!/usr/bin/env bash
|
||||
# syncro-env.sh — Resolve the repo root and the caller's Syncro API key from the SOPS vault.
|
||||
#
|
||||
# Source this instead of hardcoding a repo path or an API key:
|
||||
# source "$(dirname "${BASH_SOURCE[0]}")/syncro-env.sh" # or by absolute path
|
||||
# curl -s "$SYNCRO_BASE/customers?api_key=$SYNCRO_API_KEY"
|
||||
#
|
||||
# Exports: CLAUDETOOLS_ROOT, VAULT_ROOT, SYNCRO_USER, SYNCRO_BASE, SYNCRO_API_KEY
|
||||
#
|
||||
# Root resolution mirrors vault.sh: derive from this script's own location, then let
|
||||
# identity.json's claudetools_root override. Never hardcode a drive letter — the repo
|
||||
# lives at C:/claudetools on some machines and D:/claudetools on others.
|
||||
#
|
||||
# Soft-fails like get-identity.sh: on any failure it warns, leaves SYNCRO_API_KEY empty,
|
||||
# and returns 1 without exiting, so a sourcing skill degrades instead of dying.
|
||||
|
||||
_syncro_env_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
export CLAUDETOOLS_ROOT="$(cd "$_syncro_env_dir/../.." && pwd)"
|
||||
_identity="$CLAUDETOOLS_ROOT/.claude/identity.json"
|
||||
|
||||
export SYNCRO_BASE="https://computerguru.syncromsp.com/api/v1"
|
||||
export SYNCRO_API_KEY=""
|
||||
export SYNCRO_USER=""
|
||||
export VAULT_ROOT=""
|
||||
|
||||
if [ ! -f "$_identity" ]; then
|
||||
echo "[WARNING] identity.json not found at $_identity — Syncro calls will be unauthenticated" >&2
|
||||
return 1 2>/dev/null || exit 1
|
||||
fi
|
||||
|
||||
# identity.json is authoritative for both the repo root and the vault location.
|
||||
_root_override=$(jq -r '.claudetools_root // empty' "$_identity" 2>/dev/null)
|
||||
[ -n "$_root_override" ] && [ -d "$_root_override" ] && export CLAUDETOOLS_ROOT="$_root_override"
|
||||
export VAULT_ROOT=$(jq -r '.vault_path // empty' "$_identity" 2>/dev/null)
|
||||
export SYNCRO_USER=$(jq -r '.user // empty' "$_identity" 2>/dev/null)
|
||||
|
||||
# Per-user token — attribution in Syncro depends on using the right one.
|
||||
case "$SYNCRO_USER" in
|
||||
mike) _vault_path="msp-tools/syncro" ;;
|
||||
howard) _vault_path="msp-tools/syncro-howard" ;;
|
||||
*) echo "[WARNING] No Syncro key vaulted for user '$SYNCRO_USER' — enrichment skipped" >&2
|
||||
return 1 2>/dev/null || exit 1 ;;
|
||||
esac
|
||||
|
||||
SYNCRO_API_KEY=$(bash "$CLAUDETOOLS_ROOT/.claude/scripts/vault.sh" \
|
||||
get-field "$_vault_path" credentials.credential 2>/dev/null | tail -1)
|
||||
export SYNCRO_API_KEY
|
||||
|
||||
if [ -z "$SYNCRO_API_KEY" ]; then
|
||||
echo "[ERROR] Could not read $_vault_path from vault — is SOPS/age configured?" >&2
|
||||
bash "$CLAUDETOOLS_ROOT/.claude/scripts/log-skill-error.sh" "syncro-env" \
|
||||
"vault read failed for $_vault_path" --context "user=$SYNCRO_USER" >/dev/null 2>&1 || true
|
||||
return 1 2>/dev/null || exit 1
|
||||
fi
|
||||
|
||||
unset _syncro_env_dir _identity _root_override _vault_path
|
||||
return 0 2>/dev/null || true
|
||||
Reference in New Issue
Block a user