fix: edge-control: per-caller attribution for register/deregister (#1094)
- register.sh parses --as <tag> from forced-command argv, stores as registered_by in registry entries (defaults to "unknown") - allocate_port() accepts optional registered_by parameter - list output includes registered_by for each tunnel - deregister response includes deregistered_by - install.sh accepts --admin-tag <name> (defaults to "admin") and wires it into the forced-command entry as --as <tag> Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
47fb08524c
commit
1835750b0d
3 changed files with 41 additions and 10 deletions
|
|
@ -44,6 +44,7 @@ REGISTRY_DIR="/var/lib/disinto"
|
||||||
CADDY_VERSION="2.8.4"
|
CADDY_VERSION="2.8.4"
|
||||||
DOMAIN_SUFFIX="disinto.ai"
|
DOMAIN_SUFFIX="disinto.ai"
|
||||||
EXTRA_CADDYFILE="/etc/caddy/extra.d/*.caddy"
|
EXTRA_CADDYFILE="/etc/caddy/extra.d/*.caddy"
|
||||||
|
ADMIN_TAG="admin"
|
||||||
|
|
||||||
usage() {
|
usage() {
|
||||||
cat <<EOF
|
cat <<EOF
|
||||||
|
|
@ -57,6 +58,7 @@ Options:
|
||||||
--domain-suffix <suffix> Domain suffix for tunnels (default: disinto.ai)
|
--domain-suffix <suffix> Domain suffix for tunnels (default: disinto.ai)
|
||||||
--extra-caddyfile <path> Import path for operator-owned Caddy config
|
--extra-caddyfile <path> Import path for operator-owned Caddy config
|
||||||
(default: /etc/caddy/extra.d/*.caddy)
|
(default: /etc/caddy/extra.d/*.caddy)
|
||||||
|
--admin-tag <name> Caller tag for the initial admin key (default: admin)
|
||||||
-h, --help Show this help
|
-h, --help Show this help
|
||||||
|
|
||||||
Example:
|
Example:
|
||||||
|
|
@ -91,6 +93,10 @@ while [[ $# -gt 0 ]]; do
|
||||||
EXTRA_CADDYFILE="$2"
|
EXTRA_CADDYFILE="$2"
|
||||||
shift 2
|
shift 2
|
||||||
;;
|
;;
|
||||||
|
--admin-tag)
|
||||||
|
ADMIN_TAG="$2"
|
||||||
|
shift 2
|
||||||
|
;;
|
||||||
-h|--help)
|
-h|--help)
|
||||||
usage
|
usage
|
||||||
;;
|
;;
|
||||||
|
|
@ -404,8 +410,8 @@ if [ -n "$ADMIN_PUBKEY" ]; then
|
||||||
KEY_TYPE="${ADMIN_PUBKEY%% *}"
|
KEY_TYPE="${ADMIN_PUBKEY%% *}"
|
||||||
KEY_DATA="${ADMIN_PUBKEY#* }"
|
KEY_DATA="${ADMIN_PUBKEY#* }"
|
||||||
|
|
||||||
# Create forced command entry
|
# Create forced command entry with caller attribution tag
|
||||||
FORCED_CMD="restrict,command=\"${INSTALL_DIR}/register.sh\" ${KEY_TYPE} ${KEY_DATA}"
|
FORCED_CMD="restrict,command=\"${INSTALL_DIR}/register.sh --as ${ADMIN_TAG}\" ${KEY_TYPE} ${KEY_DATA}"
|
||||||
|
|
||||||
# Replace the pubkey line
|
# Replace the pubkey line
|
||||||
echo "$FORCED_CMD" > /home/disinto-register/.ssh/authorized_keys
|
echo "$FORCED_CMD" > /home/disinto-register/.ssh/authorized_keys
|
||||||
|
|
|
||||||
|
|
@ -54,13 +54,14 @@ _registry_write() {
|
||||||
}
|
}
|
||||||
|
|
||||||
# Allocate a port for a project
|
# Allocate a port for a project
|
||||||
# Usage: allocate_port <project> <pubkey> <fqdn>
|
# Usage: allocate_port <project> <pubkey> <fqdn> [<registered_by>]
|
||||||
# Returns: port number on stdout
|
# Returns: port number on stdout
|
||||||
# Writes: registry.json with project entry
|
# Writes: registry.json with project entry
|
||||||
allocate_port() {
|
allocate_port() {
|
||||||
local project="$1"
|
local project="$1"
|
||||||
local pubkey="$2"
|
local pubkey="$2"
|
||||||
local fqdn="$3"
|
local fqdn="$3"
|
||||||
|
local registered_by="${4:-unknown}"
|
||||||
|
|
||||||
_ensure_registry_dir
|
_ensure_registry_dir
|
||||||
|
|
||||||
|
|
@ -116,11 +117,13 @@ allocate_port() {
|
||||||
--arg pubkey "$pubkey" \
|
--arg pubkey "$pubkey" \
|
||||||
--arg fqdn "$fqdn" \
|
--arg fqdn "$fqdn" \
|
||||||
--arg timestamp "$timestamp" \
|
--arg timestamp "$timestamp" \
|
||||||
|
--arg registered_by "$registered_by" \
|
||||||
'.projects[$project] = {
|
'.projects[$project] = {
|
||||||
"port": $port,
|
"port": $port,
|
||||||
"fqdn": $fqdn,
|
"fqdn": $fqdn,
|
||||||
"pubkey": $pubkey,
|
"pubkey": $pubkey,
|
||||||
"registered_at": $timestamp
|
"registered_at": $timestamp,
|
||||||
|
"registered_by": $registered_by
|
||||||
}')
|
}')
|
||||||
|
|
||||||
_registry_write "$new_registry"
|
_registry_write "$new_registry"
|
||||||
|
|
@ -184,7 +187,7 @@ list_ports() {
|
||||||
local registry
|
local registry
|
||||||
registry=$(_registry_read)
|
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
|
# Get full project info from registry
|
||||||
|
|
|
||||||
|
|
@ -5,6 +5,10 @@
|
||||||
# This script runs as a forced command for the disinto-register SSH user.
|
# This script runs as a forced command for the disinto-register SSH user.
|
||||||
# It parses SSH_ORIGINAL_COMMAND and dispatches to register|deregister|list.
|
# It parses SSH_ORIGINAL_COMMAND and dispatches to register|deregister|list.
|
||||||
#
|
#
|
||||||
|
# Per-caller attribution: each admin key's forced-command passes --as <tag>,
|
||||||
|
# which is stored as registered_by in the registry. Missing --as defaults to
|
||||||
|
# "unknown" for backwards compatibility.
|
||||||
|
#
|
||||||
# Usage (via SSH):
|
# Usage (via SSH):
|
||||||
# ssh disinto-register@edge "register <project> <pubkey>"
|
# ssh disinto-register@edge "register <project> <pubkey>"
|
||||||
# ssh disinto-register@edge "deregister <project>"
|
# ssh disinto-register@edge "deregister <project>"
|
||||||
|
|
@ -37,16 +41,31 @@ AUDIT_LOG="${AUDIT_LOG:-/var/log/disinto/edge-register.log}"
|
||||||
# Captured error from check_allowlist (used for JSON response)
|
# Captured error from check_allowlist (used for JSON response)
|
||||||
_ALLOWLIST_ERROR=""
|
_ALLOWLIST_ERROR=""
|
||||||
|
|
||||||
|
# Caller tag (set via --as <tag> 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.
|
# Append one line to the audit log.
|
||||||
# Usage: audit_log <action> <project> <port> <pubkey_fp>
|
# Usage: audit_log <action> <project> <port> <pubkey_fp>
|
||||||
# Fails silently — write errors are warned but never abort.
|
# Fails silently — write errors are warned but never abort.
|
||||||
audit_log() {
|
audit_log() {
|
||||||
local action="$1" project="$2" port="$3" pubkey_fp="$4"
|
local action="$1" project="$2" port="$3" pubkey_fp="$4"
|
||||||
local timestamp caller
|
local timestamp
|
||||||
timestamp=$(date -u +"%Y-%m-%dT%H:%M:%SZ")
|
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
|
# Ensure log directory exists
|
||||||
local log_dir
|
local log_dir
|
||||||
|
|
@ -176,7 +195,7 @@ do_register() {
|
||||||
|
|
||||||
# Allocate port (idempotent - returns existing if already registered)
|
# Allocate port (idempotent - returns existing if already registered)
|
||||||
local port
|
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 Caddy route for main project domain
|
||||||
add_route "$project" "$port"
|
add_route "$project" "$port"
|
||||||
|
|
@ -216,6 +235,9 @@ do_register() {
|
||||||
do_deregister() {
|
do_deregister() {
|
||||||
local project="$1"
|
local project="$1"
|
||||||
|
|
||||||
|
# Record who is deregistering before removal
|
||||||
|
local deregistered_by="$CALLER"
|
||||||
|
|
||||||
# Get current port and pubkey before removing
|
# Get current port and pubkey before removing
|
||||||
local port pubkey_fp
|
local port pubkey_fp
|
||||||
port=$(get_port "$project")
|
port=$(get_port "$project")
|
||||||
|
|
@ -257,7 +279,7 @@ do_deregister() {
|
||||||
audit_log "deregister" "$project" "$port" "$pubkey_fp"
|
audit_log "deregister" "$project" "$port" "$pubkey_fp"
|
||||||
|
|
||||||
# Return JSON response
|
# 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
|
# List all registered tunnels
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue