fix: edge-control: per-caller attribution for register/deregister (#1094)
All checks were successful
ci/woodpecker/push/ci Pipeline was successful
ci/woodpecker/pr/ci Pipeline was successful
ci/woodpecker/pr/edge-subpath Pipeline was successful

- 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:
Claude 2026-04-20 19:29:15 +00:00
parent 2fd4da6b64
commit 037da487f7
3 changed files with 39 additions and 7 deletions

View file

@ -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 <<EOF
@ -57,6 +58,7 @@ Options:
--domain-suffix <suffix> Domain suffix for tunnels (default: disinto.ai)
--extra-caddyfile <path> Import path for operator-owned Caddy config
(default: /etc/caddy/extra.d/*.caddy)
--admin-tag <name> 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
;;
@ -368,8 +374,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

View file

@ -54,13 +54,14 @@ _registry_write() {
}
# 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
# 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

View file

@ -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 <tag>,
# 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 <project> <pubkey>"
# ssh disinto-register@edge "deregister <project>"
@ -34,6 +38,22 @@ ALLOWLIST_FILE="${ALLOWLIST_FILE:-/var/lib/disinto/allowlist.json}"
# Captured error from check_allowlist (used for JSON response)
_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
# Print usage
usage() {
cat <<EOF
@ -144,7 +164,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"
@ -179,6 +199,9 @@ do_register() {
do_deregister() {
local project="$1"
# Record who is deregistering before removal
local deregistered_by="$CALLER"
# Get current port before removing
local port
port=$(get_port "$project")
@ -210,7 +233,7 @@ do_deregister() {
reload_caddy
# 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