diff --git a/falco/config/falco.yaml b/falco/config/falco.yaml new file mode 100644 index 0000000..d1bb844 --- /dev/null +++ b/falco/config/falco.yaml @@ -0,0 +1,23 @@ +# Overrides merged on top of Falco's built-in default config. +# Enables the Prometheus /metrics endpoint so netdata can scrape rule-trigger +# counts and alert via the existing Telegram notifier. +webserver: + enabled: true + listen_port: 8765 + k8s_healthz_endpoint: /healthz + prometheus_metrics_enabled: true + +metrics: + enabled: true + interval: 15s + output_rule: true + rules_counters_enabled: true + +# Falco's default is "first rule wins" per event type, for performance - +# confirmed by testing: an execve matching both the stock "Drop and execute +# new binary in container" rule AND our custom miner-detect rule only +# emitted the stock rule's alert, silently. `all` disables that +# short-circuit so every matching rule fires independently (added in +# Falco 0.36.0). Accepting the modest extra CPU cost - a security tool +# silently dropping the alert it exists to send is worse. +rule_matching: all diff --git a/falco/docker-compose.yaml b/falco/docker-compose.yaml new file mode 100644 index 0000000..a8fdac5 --- /dev/null +++ b/falco/docker-compose.yaml @@ -0,0 +1,34 @@ +networks: + default: + # Falco's own default bridge network, plus netdata's network so netdata + # can scrape http://falco:8765/metrics by container name. + netdata_default: + external: true + +services: + falco: + image: falcosecurity/falco:0.44.1 + container_name: falco + restart: always + cap_drop: + - all + cap_add: + - sys_admin + - sys_resource + - sys_ptrace + security_opt: + - apparmor:unconfined + networks: + - default + - netdata_default + ports: + - "127.0.0.1:8765:8765" + volumes: + - /sys/kernel/tracing:/sys/kernel/tracing:ro + - /var/run/docker.sock:/host/var/run/docker.sock:ro + - /proc:/host/proc:ro + - /etc:/host/etc:ro + - ./config/falco.yaml:/etc/falco/config.d/telemetry.yaml:ro + - ./rules/miner-detect.yaml:/etc/falco/rules.d/miner-detect.yaml:ro + - ./rules/miner-pool-ports.yaml:/etc/falco/rules.d/miner-pool-ports.yaml:ro + - ./rules/tune-noise.yaml:/etc/falco/rules.d/tune-noise.yaml:ro diff --git a/falco/rules/miner-detect.yaml b/falco/rules/miner-detect.yaml new file mode 100644 index 0000000..987eef7 --- /dev/null +++ b/falco/rules/miner-detect.yaml @@ -0,0 +1,27 @@ +# Custom rule: catch known cryptominer binaries by process name directly. +# Added after the 2026-08-06 gitea/xmrig compromise, where the miner was +# invoked as `--url=host:port` with no "stratum+tcp" scheme prefix, which +# would NOT have matched Falco's stock "Detect crypto miners using the +# Stratum protocol" rule. Matching the binary name is the reliable signal. + +- list: miner_binary_names + items: [ + xmrig, xmrig-notls, xmr-stak, xmr-stak-cpu, xmr-stak-rx, + minerd, cpuminer, ccminer, cgminer, bfgminer, + nanominer, t-rex, trex, teamredminer, phoenixminer, lolminer, + ethminer, nbminer, gminer, srbminer, xmrigMiner, + ] + +- rule: Detect crypto miner binary execution + desc: > + A process matching a known cryptocurrency miner binary name was executed. + Catches unauthorized miners even when the command line doesn't use a + stratum+tcp:// style URL. + condition: > + spawned_process and proc.name in (miner_binary_names) + output: > + Cryptominer binary execution detected + (user=%user.name command=%proc.cmdline container=%container.name + image=%container.image.repository pid=%proc.pid) + priority: CRITICAL + tags: [miners, process, mitre_impact] diff --git a/falco/rules/miner-pool-ports.yaml b/falco/rules/miner-pool-ports.yaml new file mode 100644 index 0000000..fb8fb9a --- /dev/null +++ b/falco/rules/miner-pool-ports.yaml @@ -0,0 +1,28 @@ +# Supplementary defense-in-depth: outbound connections to common mining-pool +# ports. NOTE: this image's shipped falco_rules.yaml has no built-in +# miner/stratum/pool rules to "enable" (checked directly - none exist), so +# this is written from scratch rather than overriding a stock rule. It's +# a weaker signal than miner-detect.yaml's process-name match (our actual +# incident used port 10128, which isn't in this list - pools use arbitrary +# ports), kept anyway as a second layer. + +- list: miner_pool_ports + # Kept to well-corroborated fixed mining-pool ports only, all well below + # the Linux ephemeral port range (32768-60999). Two entries originally + # added here without solid sourcing (45560, 45700) fell inside that + # ephemeral range and produced an immediate false positive against + # qbittorrent_eXoDOS's own randomly-assigned outbound source port - + # removed. + items: [3333, 3334, 4444, 5555, 5556, 7777, 8888, 9999, 14444] + +- rule: Detect outbound connection to common miner pool port + desc: Outbound connection to a TCP port commonly used by cryptomining pools. + condition: > + outbound and evt.type in (connect, sendto, sendmsg) + and fd.rport in (miner_pool_ports) + output: > + Outbound connection to common miner-pool port + (user=%user.name command=%proc.cmdline connection=%fd.name + container=%container.name image=%container.image.repository) + priority: WARNING + tags: [miners, network, mitre_impact] diff --git a/falco/rules/tune-noise.yaml b/falco/rules/tune-noise.yaml new file mode 100644 index 0000000..a3916e1 --- /dev/null +++ b/falco/rules/tune-noise.yaml @@ -0,0 +1,13 @@ +# Tune out stock-rule false positives observed on this specific host, so the +# broad netdata "any Falco match" alarm only pages for things worth paging +# for. Uses Falco's own user_* customization macros rather than disabling +# whole rules, so the underlying security check stays active for everything +# else. Add entries here as new noisy defaults turn up - don't let this list +# grow into blinding Falco to anything actually meaningful. + +# "Sensitive file opened for reading by non-trusted program" fired every +# ~10s from inbox-zero-db's pg_isready healthcheck touching /etc/shadow - +# NSS/libc user-lookup behavior during process init, not credential +# harvesting. Scoped to just this binary, not a blanket rule disable. +- macro: user_known_read_sensitive_files_activities + condition: (proc.name = pg_isready)