LoFP LoFP / legitimate acme certificate renewal agents (cert-manager, certbot, caddy) use acme-tls/1 during tls-alpn-01 challenges. a single incomplete challenge due to a network timeout can occur normally, but five or more incomplete sessions from the same source within the detection window is abnormal even for misconfigured acme clients. tls decode_error or illegal_parameter alerts can also be produced by buggy tls clients or misconfigured load balancers. suppress by adding known acme client source ips or acme ca validation ip ranges to an exception list.

Techniques

Sample rules

Repeated Stalled TLS Handshakes via ALPN acme-tls/1 Extension

Description

This rule detects two ALPN-based denial-of-service patterns against TLS servers. The first identifies repeated stalled handshakes advertising the acme-tls/1 ALPN extension with no session established, indicating potential goroutine or worker exhaustion in reverse proxies. The second matches connections where a malformed ALPN extension triggers TLS alerts such as decode_error or illegal_parameter, consistent with zero-length ALPN list exploitation. Both patterns are anomalous outside of scheduled ACME TLS-ALPN-01 certificate validation activity.

Detection logic

from logs-network_traffic.tls*
| where source.ip is not null and destination.ip is not null and (
    (
      tls.detailed.client_hello.extensions.application_layer_protocol_negotiation == "acme-tls/1" and
      tls.established == false
    ) or (
      CONTAINS(TO_LOWER(tls.detailed.client_hello.extensions._unparsed_), "alpn") and
      tls.detailed.alert_types in ("decode_error", "illegal_parameter")
    )
  )
| stats
    Esql.event_count = COUNT(*),
    Esql.destination_port_values = MV_SLICE(MV_DEDUPE(TOP(destination.port, 10, "asc")), 0, 10),
    Esql.network_community_id_values = MV_SLICE(MV_DEDUPE(TOP(network.community_id, 10, "asc")), 0, 10),
    Esql.alpn_values = MV_SLICE(
      MV_DEDUPE(TOP(tls.detailed.client_hello.extensions.application_layer_protocol_negotiation, 10, "asc")),
      0,
      10
    ),
    Esql.alert_type_values = MV_SLICE(MV_DEDUPE(TOP(tls.detailed.alert_types, 10, "asc")), 0, 10)
  by source.ip, destination.ip
| where Esql.event_count >= 5
| keep
    source.ip,
    destination.ip,
    Esql.event_count,
    Esql.destination_port_values,
    Esql.network_community_id_values,
    Esql.alpn_values,
    Esql.alert_type_values