Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
48 changes: 41 additions & 7 deletions cmd/micromize/root.go
Original file line number Diff line number Diff line change
Expand Up @@ -42,14 +42,33 @@ const (
)

var (
enforce bool
verbose bool
filterNamespaces string
filterImageDigest string
disableGadgets string
exemptLabel string
enforce bool
verbose bool
filterNamespaces string
filterImageDigest string
disableGadgets string
exemptLabel string
socketDenyFamilies string
socketDenyNetlinkProtocols string
)

// defaultSocketDenyFamilies is the conservative set of socket address
// families denied out of the box by the configurable layer of socket-restrict.
// It targets families that are essentially never used by cloud-native
// application containers but periodically ship kernel LPEs. AF_ALG, AF_KEY and
// AF_NETLINK/XFRM are always blocked by the gadget's hardcoded layer and are
// intentionally omitted here. AF_PACKET (MetalLB, keepalived, tcpdump-in-pod,
// kube-proxy IPVS, Cilium) and AF_VSOCK (firecracker/kata) are also excluded —
// operators that want them blocked can opt-in via --socket-deny-families.
const defaultSocketDenyFamilies = "AF_TIPC,AF_RDS,AF_SMC,AF_CAN,AF_NFC,AF_BLUETOOTH,AF_AX25,AF_ATMPVC,AF_ATMSVC,AF_X25,AF_KCM,AF_CAIF"

// defaultSocketDenyNetlinkProtocols is intentionally empty: blocking
// NETLINK_NETFILTER would break iptables-nft / nf_tables-based CNI plugins
// (Istio CNI, kube-proxy nft, etc.). NETLINK_XFRM is already blocked by the
// gadget's hardcoded layer. Operators concerned about nf_tables LPEs can
// opt-in via --socket-deny-netlink-protocols=NETLINK_NETFILTER.
const defaultSocketDenyNetlinkProtocols = ""

var rootCmd = &cobra.Command{
Use: "micromize",
Short: "micromize is a security hardening tool for containerized applications",
Expand Down Expand Up @@ -77,6 +96,8 @@ func init() {
rootCmd.PersistentFlags().StringVar(&filterImageDigest, "filter-image-digest", "", "Filter out containers running this image digest from monitoring (e.g. sha256:abc123...)")
rootCmd.PersistentFlags().StringVar(&disableGadgets, "disable-gadgets", "", "Comma-separated list of gadgets to disable (e.g. ptrace-restrict,cap-restrict)")
rootCmd.PersistentFlags().StringVar(&exemptLabel, "exempt-label", "micromize.dev/exempt", "Kubernetes label key used to mark namespaces as exempt from monitoring (value must be 'true'). Set to empty string to disable. Changes take effect on restart.")
rootCmd.PersistentFlags().StringVar(&socketDenyFamilies, "socket-deny-families", defaultSocketDenyFamilies, "Comma-separated list of socket address families denied by socket-restrict's configurable layer. Names (e.g. AF_TIPC) or decimal numbers; case-insensitive. AF_ALG/AF_KEY/XFRM are always blocked regardless. Set to empty string to disable the configurable layer.")
rootCmd.PersistentFlags().StringVar(&socketDenyNetlinkProtocols, "socket-deny-netlink-protocols", defaultSocketDenyNetlinkProtocols, "Comma-separated list of additional AF_NETLINK protocols denied by socket-restrict. Defaults to empty (NETLINK_XFRM is always blocked). Use NETLINK_NETFILTER to block nf_tables LPE chains (incompatible with iptables-nft / nf_tables-based CNI).")
}

func run(ctx context.Context) error {
Expand Down Expand Up @@ -122,7 +143,20 @@ func run(ctx context.Context) error {
return fmt.Errorf("creating local manager operator: %w", err)
}

contextManager := gadget.NewContextManager([]operators.DataOperator{ociHandlerOp, localManagerOp, eventTypeOp, outputOp})
socketDenyFamilyList, err := operators.ParseSocketDenyFamilies(socketDenyFamilies)
if err != nil {
return fmt.Errorf("parsing --socket-deny-families: %w", err)
}
socketDenyNetlinkProtocolList, err := operators.ParseSocketDenyNetlinkProtocols(socketDenyNetlinkProtocols)
if err != nil {
return fmt.Errorf("parsing --socket-deny-netlink-protocols: %w", err)
}
slog.Info("Socket-restrict deny-list",
"families", socketDenyFamilyList,
"netlinkProtocols", socketDenyNetlinkProtocolList)
socketRestrictOp := operators.NewSocketRestrictOperator(socketDenyFamilyList, socketDenyNetlinkProtocolList)

contextManager := gadget.NewContextManager([]operators.DataOperator{ociHandlerOp, localManagerOp, socketRestrictOp, eventTypeOp, outputOp})

// Create gadget registry
registry := gadget.NewRegistry(contextManager, runtimeManager)
Expand Down
79 changes: 67 additions & 12 deletions gadgets/socket-restrict/README.md
Original file line number Diff line number Diff line change
@@ -1,24 +1,79 @@
# socket-restrict

Restrict dangerous socket primitives in containers.
Restrict dangerous socket address families and protocols in containers.

This gadget blocks all `AF_ALG` (kernel crypto userspace API) socket usage
inside containers. `AF_ALG` is rarely needed in containerized production
workloads — most TLS, SSH, and dm-crypt use cases never touch it — and
blocking it eliminates a class of kernel attack surface from the container
boundary.
The gadget enforces in two layers:

The initial motivation is CVE-2026-31431 (Copy Fail), a Linux kernel local
privilege escalation in `algif_aead` that can be triggered via `AF_ALG`
sockets. This gadget blocks the entire killchain at socket creation time,
before any vulnerable kernel path is reached.
1. **Hardcoded, always-on blocks** for the families with active kernel LPEs.
These are enforced regardless of configuration:
- `AF_ALG` — kernel crypto userspace API. Motivated by CVE-2026-31431
(Copy Fail), a privilege escalation in `algif_aead` reachable via
`AF_ALG` sockets.
- `AF_KEY` — PF_KEY IPsec key management.
- `AF_NETLINK` with protocol `NETLINK_XFRM` — XFRM/IPsec state & policy
configuration.

`AF_KEY` and `NETLINK_XFRM` are the only ways to configure XFRM/IPsec from
userspace and are the entry point for the DirtyClone killchain
(CVE-2026-43503), a kernel LPE on the XFRM/ESP packet path. Blocking them
at socket creation removes the attack surface before any vulnerable kernel
code is reached.

2. **A configurable deny-list layer** for additional address families and
`AF_NETLINK` protocols that have no legitimate cloud-native use. This layer
is driven by two BPF maps populated at startup from CLI flags, so operators
can tune coverage without rebuilding the BPF program.

## Configurable layer

| Flag | Default | Notes |
|---|---|---|
| `--socket-deny-families` | `AF_TIPC,AF_RDS,AF_SMC,AF_CAN,AF_NFC,AF_BLUETOOTH,AF_AX25,AF_ATMPVC,AF_ATMSVC,AF_X25,AF_KCM,AF_CAIF` | Additional families to deny. Names or decimal numbers, case-insensitive. `AF_ALG`/`AF_KEY`/XFRM are always blocked and need not be listed. |
| `--socket-deny-netlink-protocols` | *(empty)* | Additional `AF_NETLINK` protocols to deny. `NETLINK_XFRM` is always blocked. |

The defaults are **conservative**: only niche/legacy families with no
realistic cloud-native use are denied out of the box.

### Opt-in (compatibility-sensitive)

These are **not** denied by default because they have legitimate uses; enable
them explicitly after validating your workloads (ideally in audit mode,
`--enforce=false`, first):

| Item | Enable with | Breaks if in use |
|---|---|---|
| `AF_PACKET` | `--socket-deny-families=...,AF_PACKET` | MetalLB, keepalived, tcpdump-in-pod, kube-proxy IPVS, Cilium |
| `AF_VSOCK` | `--socket-deny-families=...,AF_VSOCK` | firecracker / kata-containers agents |
| `NETLINK_NETFILTER` | `--socket-deny-netlink-protocols=NETLINK_NETFILTER` | iptables-nft, kube-proxy nft mode, Istio CNI, nft-based CNIs |

## Recommended rollout (audit → enforce)

1. Deploy with `--enforce=false` and the default deny-lists. Watch for
`socket_family_denied_create` / `_bind` events on a representative
workload sample. Defaults should produce ~zero events on a normal data
plane.
2. Opt-in additional families/protocols incrementally, still in audit mode,
validating against your CNI / kube-proxy mode / service mesh / IPsec.
3. Switch to `--enforce=true` once the audit log is clean.

## Events

| Event | When |
|---|---|
| `af_alg_socket_create` / `af_alg_socket_bind` | `AF_ALG` (hardcoded) |
| `af_key_socket_create` | `AF_KEY` (hardcoded) |
| `xfrm_netlink_socket_create` | `AF_NETLINK`/`NETLINK_XFRM` (hardcoded) |
| `socket_family_denied_create` / `socket_family_denied_bind` | any family/protocol from the configurable deny-list |

## Hooks

| Hook | Purpose |
|---|---|
| `lsm/socket_create` | Block `AF_ALG` socket creation (main choke point) |
| `lsm/socket_bind` | Defense-in-depth: block `AF_ALG` bind if a socket FD exists from before policy load. Preserves `alg_type`/`alg_name` for visibility. |
| `lsm/socket_create` | Block denied socket families and `AF_NETLINK` protocols at creation time (main choke point). |
| `lsm/socket_bind` | Defense-in-depth: block denied binds if a socket FD existed before policy load. Preserves `alg_type`/`alg_name` for `AF_ALG` visibility. |

Both hooks preserve a prior LSM program's deny decision via the `ret`
chaining argument, so socket-restrict never overrides another LSM's block.

## Getting Started

Expand Down
3 changes: 3 additions & 0 deletions gadgets/socket-restrict/gadget.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,9 @@ datasources:
family:
annotations:
description: Socket address family
protocol:
annotations:
description: Socket protocol (e.g. NETLINK_XFRM for AF_NETLINK)
process:
annotations:
description: The process attempting a restricted socket operation
Expand Down
116 changes: 88 additions & 28 deletions gadgets/socket-restrict/program.bpf.c
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@

#include <vmlinux.h>

#include <bpf/bpf_core_read.h>
#include <gadget/buffer.h>
#include <gadget/filter.h>
#include <gadget/macros.h>
Expand All @@ -16,19 +17,52 @@ GADGET_TRACER_MAP(events, 1024 * 256);

GADGET_TRACER(socket_restrict, events, event);

// Runtime-populated deny-list of address families, populated from userspace
// at gadget init from the --socket-deny-families flag. Empty by default means
// the configurable layer is a no-op; the hardcoded AF_ALG/AF_KEY/XFRM blocks
// below always apply regardless of this map.
struct {
__uint(type, BPF_MAP_TYPE_HASH);
__uint(max_entries, MAX_DENIED_FAMILIES);
__type(key, __u16);
__type(value, __u8);
} denied_families SEC(".maps");

// Runtime-populated deny-list of AF_NETLINK protocols, from
// --socket-deny-netlink-protocols. Empty by default.
struct {
__uint(type, BPF_MAP_TYPE_HASH);
__uint(max_entries, MAX_DENIED_NETLINK_PROTOCOLS);
__type(key, __u32);
__type(value, __u8);
} denied_netlink_protocols SEC(".maps");

static __always_inline bool is_family_denied(__u16 family) {
return bpf_map_lookup_elem(&denied_families, &family) != NULL;
}

static __always_inline bool is_netlink_protocol_denied(__u32 protocol) {
return bpf_map_lookup_elem(&denied_netlink_protocols, &protocol) != NULL;
}

// Block dangerous socket families at creation — the main choke point.
//
// - AF_ALG kernel crypto userspace API (CVE-2026-31431, "Copy Fail")
// - AF_KEY PF_KEY IPsec key management
// - AF_NETLINK/XFRM XFRM/IPsec state & policy configuration
//
// AF_KEY and NETLINK_XFRM are the only ways to configure XFRM/IPsec from
// userspace and are the entry point for the DirtyClone killchain
// (CVE-2026-43503), a kernel LPE on the XFRM/ESP packet path. Blocking them
// here removes the attack surface before any vulnerable kernel code is reached.
// Two layers:
// 1. Hardcoded, always-on blocks for the families with active kernel LPEs:
// - AF_ALG kernel crypto userspace API (CVE-2026-31431)
// - AF_KEY PF_KEY IPsec key management
// - AF_NETLINK/XFRM XFRM/IPsec state & policy configuration
// AF_KEY and NETLINK_XFRM are the entry point for the DirtyClone
// killchain (CVE-2026-43503), a kernel LPE on the XFRM/ESP path.
// 2. A configurable deny-list layer (denied_families /
// denied_netlink_protocols maps) for additional families that have no
// legitimate cloud-native use. These emit the generic
// EVENT_TYPE_SOCKET_FAMILY_DENIED_* events.
SEC("lsm/socket_create")
int BPF_PROG(micromize_socket_create, int family, int type, int protocol,
int kern, int ret) {
(void)type;

// Preserve a deny decision from a previously-run LSM program in the chain.
if (ret)
return ret;
Expand All @@ -39,13 +73,20 @@ int BPF_PROG(micromize_socket_create, int family, int type, int protocol,
if (gadget_should_discard_data_current())
return 0;

__u16 fam = (__u16)family;
__u32 socket_protocol = protocol >= 0 ? (__u32)protocol : 0;

__u32 event_type;
if (family == AF_ALG) {
if (fam == AF_ALG) {
event_type = EVENT_TYPE_SOCKET_AF_ALG_CREATE;
} else if (family == AF_KEY) {
} else if (fam == AF_KEY) {
event_type = EVENT_TYPE_SOCKET_AF_KEY_CREATE;
} else if (family == AF_NETLINK && protocol == NETLINK_XFRM) {
} else if (fam == AF_NETLINK && socket_protocol == NETLINK_XFRM) {
event_type = EVENT_TYPE_SOCKET_XFRM_NETLINK_CREATE;
} else if (is_family_denied(fam)) {
event_type = EVENT_TYPE_SOCKET_FAMILY_DENIED_CREATE;
} else if (fam == AF_NETLINK && is_netlink_protocol_denied(socket_protocol)) {
event_type = EVENT_TYPE_SOCKET_FAMILY_DENIED_CREATE;
} else {
return 0;
}
Expand All @@ -61,7 +102,8 @@ int BPF_PROG(micromize_socket_create, int family, int type, int protocol,
gadget_process_populate(&event->process);
event->timestamp_raw = bpf_ktime_get_boot_ns();
event->event_type = event_type;
event->family = family;
event->family = (__u32)fam;
event->protocol = socket_protocol;
event->alg_type[0] = '\0';
event->alg_name[0] = '\0';

Expand All @@ -73,27 +115,42 @@ int BPF_PROG(micromize_socket_create, int family, int type, int protocol,
return 0;
}

// Defense-in-depth: block AF_ALG bind if a socket FD exists from before
// policy load. Preserves alg_type/alg_name for visibility.
// Defense-in-depth: block denied socket binds if a socket FD exists from
// before policy load. Preserves AF_ALG alg_type/alg_name for visibility.
SEC("lsm/socket_bind")
int BPF_PROG(micromize_socket_bind, struct socket *sock,
struct sockaddr *address, int addrlen, int ret) {
(void)sock;

// Preserve a deny decision from a previously-run LSM program in the chain.
if (ret)
return ret;

if (gadget_should_discard_data_current())
return 0;

if (!address || addrlen < SOCKADDR_ALG_TYPE_END)
if (!address || addrlen < sizeof(__u16))
return 0;

__u16 family = 0;
bpf_probe_read_kernel(&family, sizeof(family), address);
if (family != AF_ALG)

__u32 protocol = 0;
__u32 event_type;
if (family == AF_ALG) {
event_type = EVENT_TYPE_SOCKET_AF_ALG_BIND;
} else if (is_family_denied(family)) {
event_type = EVENT_TYPE_SOCKET_FAMILY_DENIED_BIND;
} else if (family == AF_NETLINK) {
// Micro-optimization: only read sk_protocol for AF_NETLINK, and only
// when the family itself was not already denied above.
struct sock *sk = BPF_CORE_READ(sock, sk);
if (sk)
protocol = (__u32)BPF_CORE_READ_BITFIELD_PROBED(sk, sk_protocol);
if (!is_netlink_protocol_denied(protocol))
return 0;
event_type = EVENT_TYPE_SOCKET_FAMILY_DENIED_BIND;
} else {
return 0;
}

struct event *event;
event = gadget_reserve_buf(&events, sizeof(*event));
Expand All @@ -105,19 +162,22 @@ int BPF_PROG(micromize_socket_bind, struct socket *sock,

gadget_process_populate(&event->process);
event->timestamp_raw = bpf_ktime_get_boot_ns();
event->event_type = EVENT_TYPE_SOCKET_AF_ALG_BIND;
event->event_type = event_type;
event->family = family;
event->protocol = protocol;
event->alg_type[0] = '\0';
event->alg_name[0] = '\0';

bpf_probe_read_kernel(event->alg_type, SOCKADDR_ALG_TYPE_LEN,
(const char *)address + SOCKADDR_ALG_TYPE_OFFSET);
event->alg_type[SOCKADDR_ALG_TYPE_LEN] = '\0';
if (family == AF_ALG && addrlen >= SOCKADDR_ALG_TYPE_END) {
bpf_probe_read_kernel(event->alg_type, SOCKADDR_ALG_TYPE_LEN,
(const char *)address + SOCKADDR_ALG_TYPE_OFFSET);
event->alg_type[SOCKADDR_ALG_TYPE_LEN] = '\0';

if (addrlen >= SOCKADDR_ALG_MIN_LEN) {
bpf_probe_read_kernel(event->alg_name, SOCKADDR_ALG_NAME_LEN,
(const char *)address + SOCKADDR_ALG_NAME_OFFSET);
event->alg_name[SOCKADDR_ALG_NAME_LEN - 1] = '\0';
} else {
event->alg_name[0] = '\0';
if (addrlen >= SOCKADDR_ALG_MIN_LEN) {
bpf_probe_read_kernel(event->alg_name, SOCKADDR_ALG_NAME_LEN,
(const char *)address + SOCKADDR_ALG_NAME_OFFSET);
event->alg_name[SOCKADDR_ALG_NAME_LEN - 1] = '\0';
}
}

gadget_submit_buf(ctx, &events, event, sizeof(*event));
Expand Down
Loading
Loading