#!/bin/sh set -eu cd "$(dirname "$0")/.." CONFIG_FILE="${DEPLOY_CONFIG:-deploy-routeros.env}" if [ -f "$CONFIG_FILE" ]; then # shellcheck disable=SC1090 case "$CONFIG_FILE" in /*) . "$CONFIG_FILE" ;; *) . "./$CONFIG_FILE" ;; esac fi VERSION="$(tr -d '[:space:]' < VERSION)" [ -n "$VERSION" ] || { echo "VERSION is empty" >&2; exit 2; } case "$VERSION" in *[!A-Za-z0-9._-]*) echo "VERSION contains unsupported characters: $VERSION" >&2; exit 2 ;; esac CONTAINER_NAME="suricata_${VERSION}" ROOT_DIR="/containers/${CONTAINER_NAME}/root" : "${ROUTER_HOST:=192.168.88.1}" : "${ROUTER_USER:=admin}" : "${ROUTER_PORT:=22}" : "${ROUTER_IDENTITY_FILE:=}" : "${ROUTER_SCP_DIR:=/}" : "${ROUTER_DISK:=disk1}" : "${CONTAINER_IP:=172.31.255.2/30}" : "${CONTAINER_GATEWAY:=172.31.255.1}" : "${CONTAINER_SUBNET:=172.31.255.0/30}" : "${CONTAINER_BRIDGE:=br-ids}" : "${CONTAINER_VETH:=veth-ids}" : "${VLAN_ID:=100}" : "${TZSP_PORT:=37008}" : "${CONFIGURE_SNIFFER:=true}" : "${START_SNIFFER:=true}" : "${SURICATA_HOME_NET:=[192.168.0.0/16,10.0.0.0/8,172.16.0.0/12]}" : "${MONITORED_NETWORKS:=192.168.0.0/16,10.0.0.0/8,172.16.0.0/12}" : "${SURICATA_LOG_MAX_MB:=512}" : "${FORENSIC_PCAP_MODE:=blocks}" : "${FORENSIC_PCAP_WINDOW_SECONDS:=60}" : "${FORENSIC_PCAP_MEMORY_MB:=64}" : "${FORENSIC_PCAP_MAX_FILES:=32}" : "${FORENSIC_PCAP_MAX_TOTAL_MB:=512}" : "${AUTO_BLOCK:=false}" : "${AUTO_BLOCK_MAX_SEVERITY:=1}" : "${BLOCK_TIMEOUT:=1h}" : "${UPDATE_RULES_ON_START:=false}" : "${SURICATA_PERSIST_LIB_DIR:=/data/lib/suricata}" : "${RULE_UPDATE_INTERVAL_HOURS:=24}" : "${ALERT_RETENTION_DAYS:=14}" : "${REDIS_MANAGED:=true}" : "${REDIS_DATA_DIR:=/data/redis}" : "${REDIS_PORT:=6379}" : "${REDIS_MAXMEMORY_MB:=0}" : "${REDIS_SNAPSHOT_SECONDS:=1800}" : "${REDIS_AOF:=true}" : "${TRAFFIC_RETENTION_HOURS:=24}" : "${TRAFFIC_MAX_EVENTS:=0}" : "${TRAFFIC_MEMORY_EVENTS:=0}" : "${WEBSOCKET_QUEUE_SIZE:=512}" : "${LIVE_FLOW_UPDATE_SECONDS:=2.0}" : "${ALERT_MAX_SEVERITY:=2}" : "${ALERT_DEDUP_WINDOW_SECONDS:=300}" : "${ALERT_IGNORE_SIDS:=1000001}" : "${ALERT_IGNORE_CATEGORIES:=}" : "${ADMIN_USERNAME:=admin}" : "${ADMIN_PASSWORD:=}" : "${METRICS_ALLOWED_IPS:=127.0.0.1/32,::1/128}" : "${METRICS_BASIC_AUTH_USERNAME:=}" : "${METRICS_BASIC_AUTH_PASSWORD:=}" : "${SESSION_HOURS:=168}" : "${SESSION_COOKIE_SECURE:=false}" : "${ANALYTICS_SNAPSHOT_INTERVAL_SECONDS:=60}" : "${NDR_ENABLED:=true}" : "${NDR_CORRELATION_WINDOW_SECONDS:=1800}" : "${BEHAVIOR_MIN_OBSERVATIONS:=50}" : "${NDR_AUTO_BLOCK:=false}" : "${NDR_AUTO_BLOCK_RISK:=92}" : "${ROUTEROS_INVENTORY_INTERVAL_SECONDS:=300}" : "${NOTIFY_WEBHOOK_URL:=}" : "${NOTIFY_MIN_RISK:=80}" : "${NOTIFY_TIMEOUT_SECONDS:=5}" : "${CREATE_REST_USER:=false}" : "${ENABLE_WWW_SSL:=false}" : "${ROUTEROS_REST_USER:=suricata-api}" : "${ROUTEROS_REST_PASSWORD:=CHANGE_ME}" : "${ROUTEROS_VERIFY_TLS:=false}" : "${ROUTEROS_ADDRESS_LIST:=IDS-BLOCK}" : "${KEEP_REMOTE_RSC:=false}" usage() { cat < The TAR must already exist on RouterOS. Example: ./scripts/upload-routeros-image.sh build/routeros-suricata-tzsp-arm64.tar $0 routeros-suricata-tzsp-arm64.tar Container name: ${CONTAINER_NAME} Root dir: ${ROOT_DIR} USAGE } IMAGE_TAR_ROS="${1:-}" if [ -z "$IMAGE_TAR_ROS" ]; then usage >&2 exit 2 fi case "$IMAGE_TAR_ROS" in *.tar) ;; *) echo "RouterOS image path must point to a .tar file: $IMAGE_TAR_ROS" >&2; exit 2 ;; esac need() { command -v "$1" >/dev/null 2>&1 || { echo "$1 is required" >&2 exit 2 } } need ssh need scp case "$ROUTER_PORT" in *[!0-9]*|'') echo "ROUTER_PORT must be numeric" >&2; exit 2 ;; esac case "$VLAN_ID" in *[!0-9]*|'') echo "VLAN_ID must be numeric" >&2; exit 2 ;; esac case "$TZSP_PORT" in *[!0-9]*|'') echo "TZSP_PORT must be numeric" >&2; exit 2 ;; esac case "$FORENSIC_PCAP_MODE" in blocks|alerts|all|off) ;; *) echo "FORENSIC_PCAP_MODE must be one of: blocks, alerts, all, off" >&2; exit 2 ;; esac if { [ -n "$METRICS_BASIC_AUTH_USERNAME" ] && [ -z "$METRICS_BASIC_AUTH_PASSWORD" ]; } || \ { [ -z "$METRICS_BASIC_AUTH_USERNAME" ] && [ -n "$METRICS_BASIC_AUTH_PASSWORD" ]; }; then echo "METRICS_BASIC_AUTH_USERNAME and METRICS_BASIC_AUTH_PASSWORD must both be set or both be empty" >&2 exit 2 fi for numeric_pair in \ "REDIS_PORT=$REDIS_PORT" \ "SURICATA_LOG_MAX_MB=$SURICATA_LOG_MAX_MB" \ "FORENSIC_PCAP_WINDOW_SECONDS=$FORENSIC_PCAP_WINDOW_SECONDS" \ "FORENSIC_PCAP_MEMORY_MB=$FORENSIC_PCAP_MEMORY_MB" \ "FORENSIC_PCAP_MAX_FILES=$FORENSIC_PCAP_MAX_FILES" \ "FORENSIC_PCAP_MAX_TOTAL_MB=$FORENSIC_PCAP_MAX_TOTAL_MB" \ "REDIS_MAXMEMORY_MB=$REDIS_MAXMEMORY_MB" \ "REDIS_SNAPSHOT_SECONDS=$REDIS_SNAPSHOT_SECONDS" \ "TRAFFIC_RETENTION_HOURS=$TRAFFIC_RETENTION_HOURS" \ "TRAFFIC_MAX_EVENTS=$TRAFFIC_MAX_EVENTS" \ "TRAFFIC_MEMORY_EVENTS=$TRAFFIC_MEMORY_EVENTS" \ "WEBSOCKET_QUEUE_SIZE=$WEBSOCKET_QUEUE_SIZE" \ "SESSION_HOURS=$SESSION_HOURS" \ "ANALYTICS_SNAPSHOT_INTERVAL_SECONDS=$ANALYTICS_SNAPSHOT_INTERVAL_SECONDS" \ "NDR_CORRELATION_WINDOW_SECONDS=$NDR_CORRELATION_WINDOW_SECONDS" \ "BEHAVIOR_MIN_OBSERVATIONS=$BEHAVIOR_MIN_OBSERVATIONS" \ "NDR_AUTO_BLOCK_RISK=$NDR_AUTO_BLOCK_RISK" \ "ROUTEROS_INVENTORY_INTERVAL_SECONDS=$ROUTEROS_INVENTORY_INTERVAL_SECONDS" \ "NOTIFY_MIN_RISK=$NOTIFY_MIN_RISK" \ "NOTIFY_TIMEOUT_SECONDS=$NOTIFY_TIMEOUT_SECONDS" do case "${numeric_pair#*=}" in *[!0-9]*|'') echo "${numeric_pair%%=*} must be numeric" >&2; exit 2 ;; esac done check_ros_value() { label="$1" value="$2" case "$value" in *'"'*|*'\\'*|*'$'*|*';'*|*'`'*) echo "$label contains a character not supported by the deploy script: $value" >&2 exit 3 ;; esac case "$value" in *"\n"*|*"\r"*) echo "$label contains a newline" >&2 exit 3 ;; esac } for pair in \ "IMAGE_TAR_ROS=$IMAGE_TAR_ROS" \ "ROUTER_DISK=$ROUTER_DISK" \ "CONTAINER_IP=$CONTAINER_IP" \ "CONTAINER_GATEWAY=$CONTAINER_GATEWAY" \ "CONTAINER_SUBNET=$CONTAINER_SUBNET" \ "CONTAINER_BRIDGE=$CONTAINER_BRIDGE" \ "CONTAINER_VETH=$CONTAINER_VETH" \ "SURICATA_HOME_NET=$SURICATA_HOME_NET" \ "MONITORED_NETWORKS=$MONITORED_NETWORKS" \ "BLOCK_TIMEOUT=$BLOCK_TIMEOUT" \ "ALERT_IGNORE_SIDS=$ALERT_IGNORE_SIDS" \ "ALERT_IGNORE_CATEGORIES=$ALERT_IGNORE_CATEGORIES" \ "ADMIN_USERNAME=$ADMIN_USERNAME" \ "ADMIN_PASSWORD=$ADMIN_PASSWORD" \ "METRICS_ALLOWED_IPS=$METRICS_ALLOWED_IPS" \ "METRICS_BASIC_AUTH_USERNAME=$METRICS_BASIC_AUTH_USERNAME" \ "METRICS_BASIC_AUTH_PASSWORD=$METRICS_BASIC_AUTH_PASSWORD" \ "SESSION_COOKIE_SECURE=$SESSION_COOKIE_SECURE" \ "REDIS_DATA_DIR=$REDIS_DATA_DIR" \ "SURICATA_PERSIST_LIB_DIR=$SURICATA_PERSIST_LIB_DIR" \ "NOTIFY_WEBHOOK_URL=$NOTIFY_WEBHOOK_URL" \ "ROUTEROS_REST_USER=$ROUTEROS_REST_USER" \ "ROUTEROS_REST_PASSWORD=$ROUTEROS_REST_PASSWORD" \ "ROUTEROS_ADDRESS_LIST=$ROUTEROS_ADDRESS_LIST" \ "LIVE_FLOW_UPDATE_SECONDS=$LIVE_FLOW_UPDATE_SECONDS" do check_ros_value "${pair%%=*}" "${pair#*=}" done SSH_TARGET="${ROUTER_USER}@${ROUTER_HOST}" ssh_run() { if [ -n "$ROUTER_IDENTITY_FILE" ]; then ssh -i "$ROUTER_IDENTITY_FILE" -p "$ROUTER_PORT" "$SSH_TARGET" "$1" else ssh -p "$ROUTER_PORT" "$SSH_TARGET" "$1" fi } scp_put() { src="$1" dst="$2" if [ -n "$ROUTER_IDENTITY_FILE" ]; then scp -i "$ROUTER_IDENTITY_FILE" -P "$ROUTER_PORT" "$src" "${SSH_TARGET}:$dst" else scp -P "$ROUTER_PORT" "$src" "${SSH_TARGET}:$dst" fi } printf '[deploy] version: %s\n' "$VERSION" printf '[deploy] container: %s\n' "$CONTAINER_NAME" printf '[deploy] image on RouterOS: %s\n' "$IMAGE_TAR_ROS" printf '[deploy] root-dir: %s\n' "$ROOT_DIR" echo "[deploy] RouterOS preflight" if ! ssh_run '/container/print' >/dev/null; then echo "RouterOS container menu is unavailable." >&2 exit 4 fi if ! ssh_run "/file/print without-paging where name=\"${IMAGE_TAR_ROS}\"" | grep -F "$IMAGE_TAR_ROS" >/dev/null 2>&1; then echo "Image TAR not found on RouterOS: $IMAGE_TAR_ROS" >&2 echo "Upload it first with scripts/upload-routeros-image.sh." >&2 exit 5 fi if [ "$CONFIGURE_SNIFFER" = "true" ]; then if ! ssh_run '/tool/sniffer/print' >/dev/null; then echo "RouterOS sniffer is unavailable." >&2 exit 4 fi fi if [ "$CREATE_REST_USER" = "true" ] && [ "$ROUTEROS_REST_PASSWORD" = "CHANGE_ME" ]; then echo "CREATE_REST_USER=true requires a real ROUTEROS_REST_PASSWORD" >&2 exit 6 fi DEPLOY_ID="$(date -u +%Y%m%d%H%M%S)" mkdir -p build LOCAL_RSC="build/deploy-${CONTAINER_NAME}-${DEPLOY_ID}.rsc" REMOTE_RSC_NAME="deploy-${CONTAINER_NAME}-${DEPLOY_ID}.rsc" REMOTE_RSC_SCP="${ROUTER_SCP_DIR%/}/${REMOTE_RSC_NAME}" [ "$ROUTER_SCP_DIR" = "/" ] && REMOTE_RSC_SCP="/${REMOTE_RSC_NAME}" # Keep persistent application state stable between versioned containers. DATA_DIR="${ROUTER_DISK}/containers/suricata-data" REST_URL="https://${CONTAINER_GATEWAY}" CONTAINER_IP_ONLY="${CONTAINER_IP%/*}" cat > "$LOCAL_RSC" <> "$LOCAL_RSC" <> "$LOCAL_RSC" <<'RSC' /ip/service/enable www-ssl RSC fi cat >> "$LOCAL_RSC" < 0) do={ :error "Container ${CONTAINER_NAME} already exists" } /container/add name="${CONTAINER_NAME}" file="${IMAGE_TAR_ROS}" interface="${CONTAINER_VETH}" root-dir="${ROOT_DIR}" mountlists=IDS_MOUNTS envlist=IDS_ENV start-on-boot=yes logging=yes # Wait until the newly added image has finished extracting, using find filters only. :local tries 0 :while (\$tries < 180) do={ :if ([:len [/container/find where name="${CONTAINER_NAME}" and status="stopped"]] > 0) do={ :set tries 999 } else={ :delay 2s :set tries (\$tries + 1) } } :if ([:len [/container/find where name="${CONTAINER_NAME}" and status="stopped"]] = 0) do={ :error "Container extraction did not reach stopped state" } /container/start [find where name="${CONTAINER_NAME}"] :delay 5s RSC if [ "$CONFIGURE_SNIFFER" = "true" ]; then cat >> "$LOCAL_RSC" <> "$LOCAL_RSC" <<'RSC' /tool/sniffer/start RSC fi fi cat >> "$LOCAL_RSC" <