#!/usr/bin/env bash # ============================================================================== # NAME # lea_priv_wrapper.sh # # SYNOPSIS # sudo /usr/local/sbin/lea_priv_wrapper.sh [] # sudo /usr/local/sbin/lea_priv_wrapper.sh curl_bash # # DESCRIPTION # Single sudoers-entry privileged command wrapper for the LEA Platform agent # service account (lea-agent). Every privileged operation performed by the # agent must be routed through this script. The script is the sole binary # referenced in the sudoers file and acts as a strict whitelist gate: unknown # keys are rejected, user-supplied arguments are validated against per-command # regular expressions before execution, and curl_bash URLs are matched against # an explicit allow-list before any network request is attempted. # # SECURITY CONSTRAINTS # 1. HTTP (non-TLS) URLs are unconditionally rejected before any allow-list # check or network activity. # 2. curl is invoked exclusively by this script with --proto '=https' and # --tlsv1.2 hard-coded. The caller supplies only the URL; no curl flags # can ever be injected. # 3. curl -k / --insecure is structurally impossible: the wrapper owns the # entire curl invocation. # 4. Every caller-supplied argument is validated against a per-command # regular expression; any non-matching argument causes an immediate exit. # 5. Only COMMAND_KEY values present in the whitelist table below are # dispatchable; all others produce exit code 1. # 6. All invocations — allowed and denied — are logged to syslog. # # SUDOERS SETUP # Add exactly one line to /etc/sudoers.d/lea-agent (create with visudo -f): # # lea ALL=(root) NOPASSWD: /usr/local/sbin/lea_priv_wrapper.sh # # POST-INSTALL COMMANDS # cp lea_priv_wrapper.sh /usr/local/sbin/lea_priv_wrapper.sh # chown root:root /usr/local/sbin/lea_priv_wrapper.sh # chmod 750 /usr/local/sbin/lea_priv_wrapper.sh # visudo -c # # COMMAND KEY TABLE # Binaries are resolved via PATH at runtime (no hardcoded paths). # # Key Command Argument regex / notes # -------------------- ----------------------------------- ------------------------------------------- # systemctl_restart systemctl restart systemd unit name (service|socket|target|…) # systemctl_stop systemctl stop systemd unit name # systemctl_start systemctl start systemd unit name # systemctl_status systemctl status systemd unit name # systemctl_enable systemctl enable systemd unit name # systemctl_disable systemctl disable systemd unit name # apt_install apt-get install -y Debian package name # apt_remove apt-get remove -y Debian package name # apt_update apt-get update (no argument) # sysctl_set sysctl -w key=value (alphanumeric key, simple value) # sysctl_get sysctl sysctl key (dot-separated alphanumeric) # useradd_system useradd --system POSIX username # groupadd_system groupadd --system POSIX group name # firewall_allow_port ufw allow port[/tcp|/udp] # firewall_deny_port ufw deny port[/tcp|/udp] # chmod_restricted chmod 700 absolute path # chown_lea chown lea:lea absolute path # journalctl_unit journalctl --no-pager -u systemd unit name # # One-liner examples: # sudo lea_priv_wrapper.sh systemctl_restart lea-agent.service # sudo lea_priv_wrapper.sh apt_install curl # sudo lea_priv_wrapper.sh apt_update # sudo lea_priv_wrapper.sh sysctl_set net.ipv4.ip_forward=1 # sudo lea_priv_wrapper.sh sysctl_get net.ipv4.ip_forward # sudo lea_priv_wrapper.sh useradd_system lea-worker # sudo lea_priv_wrapper.sh groupadd_system lea-data # sudo lea_priv_wrapper.sh firewall_allow_port 443/tcp # sudo lea_priv_wrapper.sh chmod_restricted /opt/lea/secrets # sudo lea_priv_wrapper.sh chown_lea /opt/lea/data # sudo lea_priv_wrapper.sh journalctl_unit lea-agent.service # sudo lea_priv_wrapper.sh curl_bash https://bootstrap.lea.internal/agent-update.sh # # CURL_BASH ALLOW-LIST FORMAT # URL_ALLOWLIST is a bash array defined in this script. Each entry is either: # # Exact string — the URL must match the entry byte-for-byte. # Example: "https://bootstrap.lea.internal/agent-update.sh" # # Extended regex — entry starts with '^'; matched via bash [[ =~ ]]. # Example: "^https://get\\.lea\\.io/update/[a-zA-Z0-9_.-]+\\.sh$" # # The match is attempted after the HTTP-scheme rejection check, so all entries # in the list may safely omit a scheme check of their own. # # Current allow-list entries: # Exact: https://bootstrap.lea.internal/agent-update.sh # Exact: https://get.lea.io/onboard/v2 # Regex: versioned update scripts under get.lea.io/update/ # Regex: per-tenant bootstraps under bootstrap.lea.internal/tenants/ # Regex: CDN agent update scripts under cdn.lea.io/agent/ # Regex: ^https://server/dir/[a-zA-Z0-9_-]+\.sh$ (documented wildcard example) # # EXIT CODES # 0 Success — command executed. # 1 Bad COMMAND_KEY, bad argument, or HTTP URL supplied. # 2 URL not in allow-list (curl_bash only). # 3 curl download failure (curl_bash only). # # LOGGING # Every invocation is written to syslog via logger(1) with tag "lea_priv_wrapper" # at facility auth. Allowed operations use priority auth.info; denied operations # (die path) use auth.warning. SUDO_USER is recorded as the caller identity in # every log entry. # ============================================================================== set -euo pipefail PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin readonly SCRIPT_NAME="lea_priv_wrapper" readonly CALLER="${SUDO_USER:-unknown}" readonly -a URL_ALLOWLIST=( "https://bootstrap.lea.internal/agent-update.sh" "https://get.lea.io/onboard/v2" "^https://get\\.lea\\.io/update/[a-zA-Z0-9_.-]+\\.sh$" "^https://bootstrap\\.lea\\.internal/tenants/[a-zA-Z0-9_-]+/bootstrap\\.sh$" "^https://cdn\\.lea\\.io/agent/[a-zA-Z0-9._-]+/update\\.sh$" "^https://server/dir/[a-zA-Z0-9_-]+\\.sh$" ) die() { local exit_code="$1"; shift local message="$*" logger -t "${SCRIPT_NAME}" -p "auth.warning" "DENIED caller=${CALLER} msg=${message}" printf '%s: %s\n' "${SCRIPT_NAME}" "${message}" >&2 exit "${exit_code}" } log_allow() { logger -t "${SCRIPT_NAME}" -p "auth.info" "ALLOWED caller=${CALLER} $*" } url_in_allowlist() { local url="$1" local entry for entry in "${URL_ALLOWLIST[@]}"; do if [[ "${entry:0:1}" == "^" ]]; then [[ "${url}" =~ ${entry} ]] && return 0 else [[ "${url}" == "${entry}" ]] && return 0 fi done return 1 } print_help() { cat < [] sudo ${SCRIPT_NAME} curl_bash Command keys: systemctl_restart systemctl restart systemctl_stop systemctl stop systemctl_start systemctl start systemctl_status systemctl status systemctl_enable systemctl enable systemctl_disable systemctl disable apt_install apt-get install -y apt_remove apt-get remove -y apt_update apt-get update sysctl_set sysctl -w sysctl_get sysctl useradd_system useradd --system groupadd_system groupadd --system firewall_allow_port ufw allow firewall_deny_port ufw deny chmod_restricted chmod 700 chown_lea chown lea:lea journalctl_unit journalctl --no-pager -u curl_bash fetch HTTPS URL and execute via bash curl_bash allow-list: Exact entries match byte-for-byte. Entries starting with '^' are matched as extended regular expressions. Exit codes: 0 success 1 bad COMMAND_KEY, bad argument, or HTTP URL supplied 2 URL not in allow-list 3 curl download failure EOF } [[ $# -ge 1 ]] || die 1 "usage: ${SCRIPT_NAME} []" if [[ "$1" == "--help" || "$1" == "-h" ]]; then print_help exit 0 fi COMMAND_KEY="$1" shift if [[ "${COMMAND_KEY}" == "curl_bash" ]]; then [[ $# -eq 1 ]] || die 1 "curl_bash requires exactly one URL argument" URL="$1" [[ "${URL}" =~ ^https:// ]] || die 1 "HTTP URLs rejected; HTTPS required (got: ${URL})" url_in_allowlist "${URL}" || die 2 "URL not in allow-list: ${URL}" TMPFILE="$(mktemp /tmp/lea_priv_XXXXXX.sh)" trap 'rm -f "${TMPFILE}"' EXIT curl \ --proto '=https' \ --tlsv1.2 \ --silent \ --show-error \ --fail \ --output "${TMPFILE}" \ "${URL}" || die 3 "curl download failed for URL: ${URL}" log_allow "curl_bash url=${URL}" bash "${TMPFILE}" exit 0 fi CMD=() ARG_REGEX="" NO_ARG=false case "${COMMAND_KEY}" in systemctl_restart) CMD=(systemctl restart) ARG_REGEX='^[a-zA-Z0-9_@.-]+\.(service|socket|target|timer|mount|slice)$' ;; systemctl_stop) CMD=(systemctl stop) ARG_REGEX='^[a-zA-Z0-9_@.-]+\.(service|socket|target|timer|mount|slice)$' ;; systemctl_start) CMD=(systemctl start) ARG_REGEX='^[a-zA-Z0-9_@.-]+\.(service|socket|target|timer|mount|slice)$' ;; systemctl_status) CMD=(systemctl status) ARG_REGEX='^[a-zA-Z0-9_@.-]+\.(service|socket|target|timer|mount|slice)$' ;; systemctl_enable) CMD=(systemctl enable) ARG_REGEX='^[a-zA-Z0-9_@.-]+\.(service|socket|target|timer|mount|slice)$' ;; systemctl_disable) CMD=(systemctl disable) ARG_REGEX='^[a-zA-Z0-9_@.-]+\.(service|socket|target|timer|mount|slice)$' ;; apt_install) CMD=(apt-get install -y) ARG_REGEX='^[a-zA-Z0-9][a-zA-Z0-9+._-]{1,127}$' ;; apt_remove) CMD=(apt-get remove -y) ARG_REGEX='^[a-zA-Z0-9][a-zA-Z0-9+._-]{1,127}$' ;; apt_update) CMD=(apt-get update) NO_ARG=true ;; sysctl_set) CMD=(sysctl -w) ARG_REGEX='^[a-zA-Z0-9_.]+=[a-zA-Z0-9_./]+$' ;; sysctl_get) CMD=(sysctl) ARG_REGEX='^[a-zA-Z0-9_.]+$' ;; useradd_system) CMD=(useradd --system) ARG_REGEX='^[a-z_][a-z0-9_-]{0,31}$' ;; groupadd_system) CMD=(groupadd --system) ARG_REGEX='^[a-z_][a-z0-9_-]{0,31}$' ;; firewall_allow_port) CMD=(ufw allow) ARG_REGEX='^[0-9]{1,5}(/(tcp|udp))?$' ;; firewall_deny_port) CMD=(ufw deny) ARG_REGEX='^[0-9]{1,5}(/(tcp|udp))?$' ;; chmod_restricted) CMD=(chmod 700) ARG_REGEX='^(/[a-zA-Z0-9._-]+)+$' ;; chown_lea) CMD=(chown lea:lea) ARG_REGEX='^(/[a-zA-Z0-9._-]+)+$' ;; journalctl_unit) CMD=(journalctl --no-pager -u) ARG_REGEX='^[a-zA-Z0-9_@.-]+\.(service|socket|target|timer|mount|slice)$' ;; *) die 1 "unknown COMMAND_KEY: ${COMMAND_KEY}" ;; esac if [[ "${NO_ARG}" == "true" ]]; then [[ $# -eq 0 ]] || die 1 "${COMMAND_KEY} accepts no argument" log_allow "cmd=${COMMAND_KEY}" "${CMD[@]}" else [[ $# -eq 1 ]] || die 1 "${COMMAND_KEY} requires exactly one argument" ARG="$1" [[ "${ARG}" =~ ${ARG_REGEX} ]] || die 1 "argument failed validation for ${COMMAND_KEY}: ${ARG}" log_allow "cmd=${COMMAND_KEY} arg=${ARG}" "${CMD[@]}" "${ARG}" fi