Skip to content
Draft
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
13 changes: 11 additions & 2 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -196,6 +196,10 @@ image-rthooks:
@echo "Push like this when ready:"
@echo "${CONTAINER_ENGINE} push cilium/tetragon-rthooks:$(DOCKER_IMAGE_TAG)"

.PHONY: image-tester-progs
image-tester-progs: ## Build the tester-progs workload image for e2e tests.
$(CONTAINER_ENGINE) build -f contrib/tester-progs/Dockerfile -t "$(E2E_TESTER_PROGS)" --platform=linux/${TARGET_ARCH} .

.PHONY: image-test
image-test: image-clang
$(CONTAINER_ENGINE) build -f Dockerfile.test -t "cilium/tetragon-test:${DOCKER_IMAGE_TAG}" .
Expand Down Expand Up @@ -326,6 +330,8 @@ E2E_AGENT ?= "cilium/tetragon:$(DOCKER_IMAGE_TAG)"
E2E_OPERATOR ?= "cilium/tetragon-operator:$(DOCKER_IMAGE_TAG)"
# RTHooks image to use for end-to-end tests
E2E_RTHOOKS ?= "cilium/tetragon-rthooks:$(DOCKER_IMAGE_TAG)"
# Tester-progs workload image to use for end-to-end tests
E2E_TESTER_PROGS ?= "cilium/tetragon-tester-progs:$(DOCKER_IMAGE_TAG)"
# BTF file to use in the E2E test. Set to nothing to use system BTF.
E2E_BTF ?= ""
# Actual flags to use for BTF file in e2e test. Use E2E_BTF instead.
Expand All @@ -348,11 +354,14 @@ ls-e2e-test:
## e2e-test E2E_TESTS=./tests/e2e/tests/skeleton: ## run a specific e2e test
.PHONY: e2e-test
ifneq ($(E2E_BUILD_IMAGES), 0)
e2e-test: image image-operator
e2e-test: image image-operator image-tester-progs
else
e2e-test:
# The tester-progs workload image is not published anywhere, so build it even
# when E2E_BUILD_IMAGES=0.
e2e-test: image-tester-progs
endif
$(GO) list $(E2E_TESTS) | xargs -Ipkg $(GO) test $(GOFLAGS) -gcflags=$(GO_BUILD_GCFLAGS) -timeout $(E2E_TEST_TIMEOUT) -failfast -cover pkg ${EXTRA_TESTFLAGS} -fail-fast \
-tetragon.tester-progs-image="$(E2E_TESTER_PROGS)" \
-tetragon.helm.set tetragon.image.override="$(E2E_AGENT)" \
-tetragon.helm.set tetragonOperator.image.override="$(E2E_OPERATOR)" \
-tetragon.helm.set tetragon.gops.enabled=true \
Expand Down
3 changes: 3 additions & 0 deletions cmd/tetragon/main_k8s.go
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,9 @@ func initK8s(ctx context.Context) (watcher.PodAccessor, error) {
if err := EnableCgIDmap(podEvents); err != nil {
return nil, err
}
if err := EnableUprobeResolvePathInContainer(podEvents); err != nil {
return nil, err
}
if option.Config.EnablePolicyFilter {
if pfState, pfErr := policyfilter.GetState(); pfErr == nil {
if err := pfState.RegisterPodHandlers(podEvents); err != nil {
Expand Down
15 changes: 12 additions & 3 deletions cmd/tetragon/main_k8s_unix.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,11 +11,11 @@ import (
"github.com/cilium/tetragon/pkg/cgidmap"
"github.com/cilium/tetragon/pkg/manager/events"
"github.com/cilium/tetragon/pkg/option"
"github.com/cilium/tetragon/pkg/sensors/tracing"
)

// EnableCgIDmap wires cgidmap into the supplied pod-event source
// when the cgidmap feature is enabled. cgidmap is unavailable on Windows, so
// this function is built only for non-Windows targets.
// EnableCgIDmap wires cgidmap into the pod-event source when the feature is
// enabled. cgidmap is unavailable on Windows.
func EnableCgIDmap(src events.PodEventSource) error {
if !option.Config.EnableCgIDmap {
return nil
Expand All @@ -25,3 +25,12 @@ func EnableCgIDmap(src events.PodEventSource) error {
}
return nil
}

// EnableUprobeResolvePathInContainer wires the resolvePathInContainer uprobe
// pod-event handlers into the pod-event source.
func EnableUprobeResolvePathInContainer(src events.PodEventSource) error {
if err := tracing.RegisterResolvePathInContainerPodHandlers(src); err != nil {
return fmt.Errorf("failed to register resolvePathInContainer uprobe pod handlers: %w", err)
}
return nil
}
9 changes: 6 additions & 3 deletions cmd/tetragon/main_k8s_windows.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,10 @@ package main

import "github.com/cilium/tetragon/pkg/manager/events"

// EnableCgIDmap is a no-op on Windows because the cgidmap
// package is unavailable there. The shape matches the unix variant so the
// caller in main_k8s.go can call it unconditionally.
// EnableCgIDmap is a no-op on Windows, where the cgidmap package is
// unavailable; it matches the unix variant's shape.
func EnableCgIDmap(_ events.PodEventSource) error { return nil }

// EnableUprobeResolvePathInContainer is a no-op on Windows, where the tracing
// pod-event handlers are unavailable; it matches the unix variant's shape.
func EnableUprobeResolvePathInContainer(_ events.PodEventSource) error { return nil }
42 changes: 42 additions & 0 deletions contrib/tester-progs/Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
# SPDX-License-Identifier: Apache-2.0

# Builds the complete tester-progs payload through its canonical Makefile.
# The repository root is the build context because Go helpers use the root Go
# module and vendor tree. Build with: make image-tester-progs.
FROM docker.io/library/golang:1.26.5@sha256:079e59808d2d252516e27e3f3a9c003740dee7f75e55aa71528766d52bcfc16a AS build
ARG TARGETARCH
WORKDIR /go/src/github.com/cilium/tetragon

# tester-progs includes native C and Go binaries, a static 32-bit binary,
# libcap users, and programs whose BTF sidecars require pahole/llvm-objcopy.
RUN set -eux; \
case "$TARGETARCH" in \
amd64) arch_packages="gcc-multilib" ;; \
arm64) arch_packages="gcc-arm-linux-gnueabihf libc6-dev-armhf-cross" ;; \
*) echo "unsupported architecture: $TARGETARCH" >&2; exit 1 ;; \
esac; \
apt-get update; \
apt-get install -y --no-install-recommends \
dwarves \
libcap-dev \
llvm \
$arch_packages; \
if ! command -v llvm-objcopy >/dev/null; then \
ln -s "$(ls /usr/bin/llvm-objcopy-* | head -n1)" /usr/local/bin/llvm-objcopy; \
fi; \
rm -rf /var/lib/apt/lists/*

COPY . ./
RUN make -C contrib/tester-progs clean && \
make -C contrib/tester-progs -j"$(nproc)" && \
mkdir -p /out && \
cd contrib/tester-progs && \
cp $(make -s all-files) /out/

# The Makefile produces glibc-linked executables, including libcap users and
# binaries which find libuprobe.so/libtester.so through a $ORIGIN rpath.
FROM docker.io/library/debian:trixie-slim@sha256:28de0877c2189802884ccd20f15ee41c203573bd87bb6b883f5f46362d24c5c2
RUN apt-get update && \
apt-get install -y --no-install-recommends libcap2 && \
rm -rf /var/lib/apt/lists/*
COPY --from=build /out/ /usr/bin/
18 changes: 18 additions & 0 deletions contrib/tester-progs/Dockerfile.dockerignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
**

!go.mod
!go.sum

!vendor/
!vendor/**

!pkg/
!pkg/**

!contrib/
!contrib/tester-progs/
!contrib/tester-progs/Makefile
!contrib/tester-progs/*.c
!contrib/tester-progs/*.h
!contrib/tester-progs/go/
!contrib/tester-progs/go/**
19 changes: 17 additions & 2 deletions contrib/tester-progs/Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,17 @@ PROGS = sigkill-tester \
LIBS = libuprobe.so \
libtester.so

all: $(PROGS)
# Auxiliary artifacts generated alongside tester programs. Keep this list with
# PROGS and LIBS so container images and tarballs can stage the complete test
# payload without duplicating the program inventory.
OTHER_FILES = usdt-resolve.btf \
uprobe-resolve.btf \
uprobe-null.btf \
resolve-nested-anon-struct.btf

ALL_FILES = $(PROGS) $(LIBS) $(OTHER_FILES)

all: $(PROGS) $(LIBS)

%: %.c
$(GCC) -Wall $< -o $@
Expand Down Expand Up @@ -183,6 +193,11 @@ pclntab-stripped: FORCE

.PHONY: clean
clean:
rm -f $(PROGS) $(LIBS)
rm -f $(ALL_FILES)

# Used by image and archive builders after compiling the default all target.
.PHONY: all-files
all-files:
@echo $(ALL_FILES)

FORCE:
8 changes: 8 additions & 0 deletions contrib/tester-progs/uprobe-simple.c
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,18 @@ pizza()
return 0;
}

int
__attribute__((noinline))
burger()
{
return 0;
}

int
main(int argc, char *argv[])
{
int ret = pizza();
printf("pizza() returned %d\n", ret);
printf("burger() returned %d\n", burger());
return ret;
}
62 changes: 62 additions & 0 deletions docs/content/en/docs/concepts/tracing-policy/hooks.md
Original file line number Diff line number Diff line change
Expand Up @@ -366,6 +366,68 @@ sensitive command input.
For more details on available selectors and their usage, see the
[Selectors]({{< ref "/docs/concepts/tracing-policy/selectors" >}}) documentation.

### Resolving paths inside other pods

By default the `path` of a uprobe is opened in the Tetragon agent's own mount
namespace. In Kubernetes the kernel is shared across pods but filesystems are
not, so the agent cannot see a library such as `libpam.so` that lives inside
another pod, and registering the policy fails.

Setting `resolvePathInContainer: true` changes this: `path` is interpreted relative
to the root filesystem of each container selected by the policy's
`podSelector`, and the uprobe is attached
per matching container. Pods that start after the policy is applied are
attached automatically, and the uprobe is detached when a pod stops. A
`podSelector` is required when `resolvePathInContainer` is set, and a policy
cannot mix `resolvePathInContainer` uprobes with regular uprobes.

To do this the agent resolves each container's root filesystem from the
container runtime, so the feature requires at least one of the following to be
enabled:

- **Runtime (OCI/NRI) hooks** resolve containers created *after* the policy is
applied. See [Configure Runtime Hooks]({{< ref "/docs/installation/runtime-hooks" >}}).
- **CRI** (agent flag `--enable-cri`, Helm value `tetragon.cri.enabled`)
additionally discovers and resolves containers that already exist when the
policy loads.

Enabling CRI is recommended, as it covers both already-running and newly created
containers; runtime hooks alone cover only containers created after the policy
is applied. If neither is enabled, containers cannot be resolved: they are
skipped and a warning is logged once when the policy loads.

```yaml
apiVersion: cilium.io/v1alpha1
kind: TracingPolicy
metadata:
name: "uprobe-pod"
spec:
podSelector:
matchLabels:
app: sshd
uprobes:
- path: "/usr/lib64/libpam.so.0.85.1"
symbols:
- "pam_authenticate"
resolvePathInContainer: true
```

Because uprobes are tied to a specific binary version, the `path` and symbol
must exist in the selected containers' images; a container missing the path is
skipped (with a warning) without failing the policy for the other containers.

{{< note >}}
If your goal is to observe authentication or privilege changes (who logged in,
success or failure, identity) rather than a specific user-space function's
behavior, consider kernel-side kprobe/LSM policies instead (for example the
`security_bprm_committed_creds` LSM hook and process-credential monitoring).
Kernel hooks are namespace-agnostic — one policy covers all pods with no
per-container path resolution — and are not tied to a library version. Use
`resolvePathInContainer` uprobes when you specifically need a user-space
function's arguments or return value (such as the result of `pam_authenticate`)
that the kernel cannot observe.
{{< /note >}}

## USDTs

Tetragon allows to attach and monitor USDT (User Statically-Defined Tracing) probes.
Expand Down
30 changes: 30 additions & 0 deletions docs/content/en/docs/reference/tracing-policy.md

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

15 changes: 15 additions & 0 deletions examples/tracingpolicy/uprobe-pod.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
apiVersion: cilium.io/v1alpha1
kind: TracingPolicy
metadata:
name: "uprobe-pod"
spec:
# resolvePathInContainer requires a podSelector: the uprobe attaches per matching container.
podSelector:
matchLabels:
app: sshd
uprobes:
- path: "/usr/lib64/libpam.so.0.85.1"
symbols:
- "pam_authenticate"
# Resolve path inside each container's root filesystem, not the agent's.
resolvePathInContainer: true
Original file line number Diff line number Diff line change
Expand Up @@ -5661,6 +5661,15 @@ spec:
format: int64
type: integer
type: array
resolvePathInContainer:
default: false
description: |-
ResolvePathInContainer resolves Path in the root filesystem of each
container selected by the policy's podSelector and attaches the uprobe
per matching container, instead of in the agent's mount namespace.
Requires a podSelector; container roots are resolved via runtime hooks
and/or CRI.
type: boolean
return:
default: false
description: Indicates whether to collect return value of the
Expand Down Expand Up @@ -7549,6 +7558,10 @@ spec:
type: object
type: array
type: object
x-kubernetes-validations:
- message: uprobe resolvePathInContainer requires a podSelector
rule: '!has(self.uprobes) || !self.uprobes.exists(u, u.resolvePathInContainer)
|| has(self.podSelector)'
required:
- metadata
- spec
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5661,6 +5661,15 @@ spec:
format: int64
type: integer
type: array
resolvePathInContainer:
default: false
description: |-
ResolvePathInContainer resolves Path in the root filesystem of each
container selected by the policy's podSelector and attaches the uprobe
per matching container, instead of in the agent's mount namespace.
Requires a podSelector; container roots are resolved via runtime hooks
and/or CRI.
type: boolean
return:
default: false
description: Indicates whether to collect return value of the
Expand Down Expand Up @@ -7549,6 +7558,10 @@ spec:
type: object
type: array
type: object
x-kubernetes-validations:
- message: uprobe resolvePathInContainer requires a podSelector
rule: '!has(self.uprobes) || !self.uprobes.exists(u, u.resolvePathInContainer)
|| has(self.podSelector)'
required:
- metadata
- spec
Expand Down
Loading
Loading