diff --git a/tools/edge-control/install.sh b/tools/edge-control/install.sh index 636741e..cf21456 100755 --- a/tools/edge-control/install.sh +++ b/tools/edge-control/install.sh @@ -44,6 +44,7 @@ REGISTRY_DIR="/var/lib/disinto" CADDY_VERSION="2.8.4" DOMAIN_SUFFIX="disinto.ai" EXTRA_CADDYFILE="/etc/caddy/extra.d/*.caddy" +ADMIN_TAG="admin" usage() { cat < Domain suffix for tunnels (default: disinto.ai) --extra-caddyfile Import path for operator-owned Caddy config (default: /etc/caddy/extra.d/*.caddy) + --admin-tag Caller tag for the initial admin key (default: admin) -h, --help Show this help Example: @@ -91,6 +93,10 @@ while [[ $# -gt 0 ]]; do EXTRA_CADDYFILE="$2" shift 2 ;; + --admin-tag) + ADMIN_TAG="$2" + shift 2 + ;; -h|--help) usage ;; @@ -404,8 +410,8 @@ if [ -n "$ADMIN_PUBKEY" ]; then KEY_TYPE="${ADMIN_PUBKEY%% *}" KEY_DATA="${ADMIN_PUBKEY#* }" - # Create forced command entry - FORCED_CMD="restrict,command=\"${INSTALL_DIR}/register.sh\" ${KEY_TYPE} ${KEY_DATA}" + # Create forced command entry with caller attribution tag + FORCED_CMD="restrict,command=\"${INSTALL_DIR}/register.sh --as ${ADMIN_TAG}\" ${KEY_TYPE} ${KEY_DATA}" # Replace the pubkey line echo "$FORCED_CMD" > /home/disinto-register/.ssh/authorized_keys diff --git a/tools/edge-control/lib/ports.sh b/tools/edge-control/lib/ports.sh index 7fe447f..1f5efac 100755 --- a/tools/edge-control/lib/ports.sh +++ b/tools/edge-control/lib/ports.sh @@ -54,13 +54,14 @@ _registry_write() { } # Allocate a port for a project -# Usage: allocate_port +# Usage: allocate_port [] # Returns: port number on stdout # Writes: registry.json with project entry allocate_port() { local project="$1" local pubkey="$2" local fqdn="$3" + local registered_by="${4:-unknown}" _ensure_registry_dir @@ -116,11 +117,13 @@ allocate_port() { --arg pubkey "$pubkey" \ --arg fqdn "$fqdn" \ --arg timestamp "$timestamp" \ + --arg registered_by "$registered_by" \ '.projects[$project] = { "port": $port, "fqdn": $fqdn, "pubkey": $pubkey, - "registered_at": $timestamp + "registered_at": $timestamp, + "registered_by": $registered_by }') _registry_write "$new_registry" @@ -184,7 +187,7 @@ list_ports() { local registry registry=$(_registry_read) - echo "$registry" | jq -r '.projects | to_entries | map({name: .key, port: .value.port, fqdn: .value.fqdn}) | .[] | @json' 2>/dev/null + echo "$registry" | jq -r '.projects | to_entries | map({name: .key, port: .value.port, fqdn: .value.fqdn, registered_by: (.value.registered_by // "unknown")}) | .[] | @json' 2>/dev/null } # Get full project info from registry diff --git a/tools/edge-control/register.sh b/tools/edge-control/register.sh index b104ebd..298ae0b 100755 --- a/tools/edge-control/register.sh +++ b/tools/edge-control/register.sh @@ -5,6 +5,10 @@ # This script runs as a forced command for the disinto-register SSH user. # It parses SSH_ORIGINAL_COMMAND and dispatches to register|deregister|list. # +# Per-caller attribution: each admin key's forced-command passes --as , +# which is stored as registered_by in the registry. Missing --as defaults to +# "unknown" for backwards compatibility. +# # Usage (via SSH): # ssh disinto-register@edge "register " # ssh disinto-register@edge "deregister " @@ -37,16 +41,31 @@ AUDIT_LOG="${AUDIT_LOG:-/var/log/disinto/edge-register.log}" # Captured error from check_allowlist (used for JSON response) _ALLOWLIST_ERROR="" +# Caller tag (set via --as in forced command) +CALLER="unknown" + +# Parse script arguments (from forced command, not SSH_ORIGINAL_COMMAND) +while [[ $# -gt 0 ]]; do + case $1 in + --as) + CALLER="$2" + shift 2 + ;; + *) + shift + ;; + esac +done + # Append one line to the audit log. # Usage: audit_log # Fails silently — write errors are warned but never abort. audit_log() { local action="$1" project="$2" port="$3" pubkey_fp="$4" - local timestamp caller + local timestamp timestamp=$(date -u +"%Y-%m-%dT%H:%M:%SZ") - caller="${SSH_USERNAME:-${SUDO_USER:-${USER:-unknown}}}" - local line="${timestamp} ${action} project=${project} port=${port} pubkey_fp=${pubkey_fp} caller=${caller}" + local line="${timestamp} ${action} project=${project} port=${port} pubkey_fp=${pubkey_fp} caller=${CALLER}" # Ensure log directory exists local log_dir @@ -176,7 +195,7 @@ do_register() { # Allocate port (idempotent - returns existing if already registered) local port - port=$(allocate_port "$project" "$full_pubkey" "${project}.${DOMAIN_SUFFIX}") + port=$(allocate_port "$project" "$full_pubkey" "${project}.${DOMAIN_SUFFIX}" "$CALLER") # Add Caddy route for main project domain add_route "$project" "$port" @@ -216,6 +235,9 @@ do_register() { do_deregister() { local project="$1" + # Record who is deregistering before removal + local deregistered_by="$CALLER" + # Get current port and pubkey before removing local port pubkey_fp port=$(get_port "$project") @@ -257,7 +279,7 @@ do_deregister() { audit_log "deregister" "$project" "$port" "$pubkey_fp" # Return JSON response - echo "{\"removed\":true,\"port\":${port},\"fqdn\":\"${project}.${DOMAIN_SUFFIX}\"}" + echo "{\"removed\":true,\"port\":${port},\"fqdn\":\"${project}.${DOMAIN_SUFFIX}\",\"deregistered_by\":\"${deregistered_by}\"}" } # List all registered tunnels