fix: edge-control: append-only audit log for register/deregister operations (#1095)
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

Every successful register/deregister appends one line to
/var/log/disinto/edge-register.log with space-separated key=value pairs:

  2026-04-20T14:30:12Z register   project=myproj port=20034 pubkey_fp=SHA256:… caller=alice
  2026-04-20T14:31:55Z deregister project=myproj port=20034 pubkey_fp=SHA256:… caller=alice

- Log dir /var/log/disinto/ created by install.sh (root:disinto-register, 0750)
- Log file created at install time (0640, root:disinto-register)
- Logrotate: daily rotation, 30 days retention, copytruncate
- Write failures emit a warning but do not fail the operation
- Caller derived from SSH_USERNAME > SUDO_USER > USER env vars

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Agent 2026-04-20 19:42:10 +00:00
parent 2fd4da6b64
commit 5ddf379191
2 changed files with 90 additions and 7 deletions

View file

@ -162,7 +162,43 @@ if [ ! -f "$ALLOWLIST_FILE" ]; then
fi fi
# ============================================================================= # =============================================================================
# Step 3: Install Caddy with Gandi DNS plugin # Step 3: Create audit log directory and logrotate config
# =============================================================================
log_info "Setting up audit log..."
LOG_DIR="/var/log/disinto"
LOG_FILE="${LOG_DIR}/edge-register.log"
mkdir -p "$LOG_DIR"
chown root:disinto-register "$LOG_DIR"
chmod 0750 "$LOG_DIR"
# Touch the log file so it exists from day one
touch "$LOG_FILE"
chmod 0640 "$LOG_FILE"
chown root:disinto-register "$LOG_FILE"
# Install logrotate config (daily rotation, 30 days retention)
LOGROTATE_CONF="/etc/logrotate.d/disinto-edge"
cat > "$LOGROTATE_CONF" <<EOF
${LOG_FILE} {
daily
rotate 30
compress
delaycompress
missingok
notifempty
create 0640 root disinto-register
copytruncate
}
EOF
chmod 0644 "$LOGROTATE_CONF"
log_info "Audit log: ${LOG_FILE}"
log_info "Logrotate config: ${LOGROTATE_CONF}"
# =============================================================================
# Step 4: Install Caddy with Gandi DNS plugin
# ============================================================================= # =============================================================================
log_info "Installing Caddy ${CADDY_VERSION} with Gandi DNS plugin..." log_info "Installing Caddy ${CADDY_VERSION} with Gandi DNS plugin..."
@ -293,7 +329,7 @@ systemctl restart caddy 2>/dev/null || {
log_info "Caddy configured with admin API on 127.0.0.1:2019" log_info "Caddy configured with admin API on 127.0.0.1:2019"
# ============================================================================= # =============================================================================
# Step 4: Install control plane scripts # Step 5: Install control plane scripts
# ============================================================================= # =============================================================================
log_info "Installing control plane scripts to ${INSTALL_DIR}..." log_info "Installing control plane scripts to ${INSTALL_DIR}..."
@ -315,7 +351,7 @@ chmod 750 "${INSTALL_DIR}/lib"
log_info "Control plane scripts installed" log_info "Control plane scripts installed"
# ============================================================================= # =============================================================================
# Step 5: Set up SSH authorized_keys # Step 6: Set up SSH authorized_keys
# ============================================================================= # =============================================================================
log_info "Setting up SSH authorized_keys..." log_info "Setting up SSH authorized_keys..."
@ -357,7 +393,7 @@ source "${INSTALL_DIR}/lib/authorized_keys.sh"
rebuild_authorized_keys rebuild_authorized_keys
# ============================================================================= # =============================================================================
# Step 6: Configure forced command for disinto-register # Step 7: Configure forced command for disinto-register
# ============================================================================= # =============================================================================
log_info "Configuring forced command for disinto-register..." log_info "Configuring forced command for disinto-register..."
@ -380,7 +416,7 @@ if [ -n "$ADMIN_PUBKEY" ]; then
fi fi
# ============================================================================= # =============================================================================
# Step 7: Final configuration # Step 8: Final configuration
# ============================================================================= # =============================================================================
log_info "Configuring domain suffix: ${DOMAIN_SUFFIX}" log_info "Configuring domain suffix: ${DOMAIN_SUFFIX}"

View file

@ -31,9 +31,41 @@ RESERVED_NAMES=(www api admin root mail chat forge ci edge caddy disinto registe
# Allowlist path (root-owned, never mutated by this script) # Allowlist path (root-owned, never mutated by this script)
ALLOWLIST_FILE="${ALLOWLIST_FILE:-/var/lib/disinto/allowlist.json}" ALLOWLIST_FILE="${ALLOWLIST_FILE:-/var/lib/disinto/allowlist.json}"
# Audit log path
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=""
# Append one line to the audit log.
# Usage: audit_log <action> <project> <port> <pubkey_fp>
# Fails silently — write errors are warned but never abort.
audit_log() {
local action="$1" project="$2" port="$3" pubkey_fp="$4"
local timestamp caller
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}"
# Ensure log directory exists
local log_dir
log_dir=$(dirname "$AUDIT_LOG")
if [ ! -d "$log_dir" ]; then
mkdir -p "$log_dir" 2>/dev/null || {
echo "[WARN] audit log: cannot create ${log_dir}" >&2
return 0
}
chown root:disinto-register "$log_dir" 2>/dev/null || true
chmod 0750 "$log_dir"
fi
# Append — write failure is non-fatal
if ! printf '%s\n' "$line" >> "$AUDIT_LOG" 2>/dev/null; then
echo "[WARN] audit log: failed to write to ${AUDIT_LOG}" >&2
fi
}
# Print usage # Print usage
usage() { usage() {
cat <<EOF cat <<EOF
@ -164,6 +196,11 @@ do_register() {
# Reload Caddy # Reload Caddy
reload_caddy reload_caddy
# Audit log
local pubkey_fp
pubkey_fp=$(ssh-keygen -lf /dev/stdin <<<"$full_pubkey" 2>/dev/null | awk '{print $2}') || pubkey_fp="unknown"
audit_log "register" "$project" "$port" "$pubkey_fp"
# Build JSON response # Build JSON response
local response="{\"port\":${port},\"fqdn\":\"${project}.${DOMAIN_SUFFIX}\"" local response="{\"port\":${port},\"fqdn\":\"${project}.${DOMAIN_SUFFIX}\""
if [ "$routing_mode" = "subdomain" ]; then if [ "$routing_mode" = "subdomain" ]; then
@ -179,8 +216,8 @@ do_register() {
do_deregister() { do_deregister() {
local project="$1" local project="$1"
# Get current port before removing # Get current port and pubkey before removing
local port local port pubkey_fp
port=$(get_port "$project") port=$(get_port "$project")
if [ -z "$port" ]; then if [ -z "$port" ]; then
@ -188,6 +225,13 @@ do_deregister() {
exit 1 exit 1
fi fi
pubkey_fp=$(get_project_info "$project" | jq -r '.pubkey // empty' 2>/dev/null) || pubkey_fp=""
if [ -n "$pubkey_fp" ]; then
pubkey_fp=$(ssh-keygen -lf /dev/stdin <<<"$pubkey_fp" 2>/dev/null | awk '{print $2}') || pubkey_fp="unknown"
else
pubkey_fp="unknown"
fi
# Remove from registry # Remove from registry
free_port "$project" >/dev/null free_port "$project" >/dev/null
@ -209,6 +253,9 @@ do_deregister() {
# Reload Caddy # Reload Caddy
reload_caddy reload_caddy
# Audit log
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}\"}"
} }