Skip to main content

Attribute Java Instrumentation Guide

Introduction

The Attribute Sensor collects traffic data at the kernel level using eBPF, which gives it visibility into application traffic without any code changes. Java applications are the one case where this is not sufficient on its own.

When a Java application uses TLS, the encryption is performed by the Java platform itself rather than by a shared system library. There is no library boundary for the sensor to observe, so an encrypted Java connection appears only as ciphertext. Attribute closes this gap with the Attribute Java agent, which observes the connection inside the JVM and makes the traffic visible to the sensor for attribution.

Note

This affects TLS-encrypted Java traffic only. Attribution for Java applications that communicate over unencrypted connections works with no Java agent and requires no configuration.

There are two ways to deliver the Java agent, and exactly one of them can be active at a time:

  • Dynamic instrumentation: The sensor attaches the agent to running JVMs on the node. Requires no application changes and no redeployment.

  • Admission-time injection: The Attribute Operator adds the agent to pods as they are created, so the agent is present from the moment the JVM starts.

This guide covers Kubernetes deployments only. Java instrumentation in other environments is not covered here — contact Attribute support if you need it.

About the Attribute Java Agent

The agent is deliberately narrow in what it does. It observes the point inside the Java platform where your application's traffic is encrypted and decrypted, and makes what it sees available to the Attribute Sensor already running on the node. It instruments only the Java TLS classes your application uses, never your application's own code.

It does not participate in TLS itself. It reads no keys and no certificates, registers no security provider, and has no influence on which protocol versions or cipher suites your application negotiates.

It is also small, by design rather than by tuning:

  • No background activity: The agent starts no threads, opens no network connections of its own, and polls nothing. It does work only while your application is already sending or receiving data.

  • A fixed memory footprint: Buffers are allocated once and reused rather than per operation, so instrumentation adds no ongoing garbage collection pressure. Off-heap use is roughly 8 KB per application thread that carries traffic.

  • Sub-microsecond cost per operation: The work added to a read or write is a buffer copy and a local handoff, measured in tens to a few hundred nanoseconds depending on payload size.

  • No configuration and no state: The agent reads no environment variables, no system properties, and no configuration files, and keeps no state of its own.

Most importantly, it is built to fail safe. Every instrumentation point is wrapped so that an error inside the agent is contained: instrumentation stops and your application carries on unaffected. The same is true if the sensor is not running — the agent simply produces nothing. Data is read only after your application's own read or write has already completed, so the agent does not change the content, ordering, or timing of your traffic.

Requirements

  • A Kubernetes cluster running version 1.28 or above.
  • Attribute Helm chart 0.0.96 or above, with operator 0.0.30 or above and sensor 0.0.284 or above.
  • Helm 3.8.0 or above.
  • Java 8 or above, on Linux x86-64 or arm64.
  • A HotSpot-based JVM: OpenJDK, Eclipse Temurin, Amazon Corretto, Azul Zulu, or GraalVM in JIT mode. OpenJ9 is not supported.
  • For dynamic instrumentation only: a writable /tmp directory in the application container.

Applications outside these bounds are left untouched. Instrumentation is skipped rather than failing the application.

Choosing an Instrumentation Mode

Both modes deliver the same Java agent and produce the same attribution data. They differ in how the agent reaches the JVM, which has practical consequences:

Dynamic InstrumentationAdmission-Time Injection
Applies toEvery supported JVM on every nodeOnly workloads you opt in
Requires redeploymentNoYes — pods must be recreated
Connection coverageConnections opened after the agent attachesAll connections, from application start
Kubernetes version1.28 or above1.28 or above

Dynamic instrumentation needs no application changes and no redeployment, which makes it the simpler mode to operate. Choose admission-time injection when you need complete connection coverage from the moment an application starts, or when you want to control precisely which workloads are instrumented.

The values file provided by Attribute determines which mode your deployment uses. Review it before changing either setting, and contact Attribute support if you are unsure which mode is active.

Important

The two modes must never run at the same time, or applications are instrumented twice. Admission-time injection therefore requires you to explicitly disable dynamic instrumentation, and the Helm chart refuses to install if you do not.

Dynamic Instrumentation with the Sensor

How It Works

The sensor watches for Java processes starting on the node, including processes that were already running when the sensor itself started. When it finds one, it waits for the JVM to finish initializing, then places the agent files in the application container and loads the agent into the running JVM.

Waiting for initialization to complete is deliberate. Attaching to a JVM that is still starting up can disrupt it on some Java versions, so the sensor confirms the JVM is ready before it acts. In practice a typical JVM is instrumented within roughly half a second of starting.

The application is neither restarted nor redeployed. Because the agent arrives after the application has started, it observes connections opened from that point onward; connections that were already established are not instrumented.

Configuration

Dynamic instrumentation is controlled by a single flag. When it is not disabled, the sensor instruments every supported JVM it finds, with no per-workload configuration.

To turn it off — which you must do before enabling admission-time injection — set:

sensorDisableAutoJavaInstrumentation: true

Limitations

  • Only connections opened after the agent attaches are instrumented. For a long-lived connection pool created at application startup, restart the application to bring its connections into scope, or use admission-time injection instead.
  • All supported JVMs on the node are instrumented. There is no per-workload opt-in.
  • The application container needs a writable /tmp. If it is read-only, instrumentation is skipped for that container.
  • A JVM started with -XX:+DisableAttachMechanism cannot be instrumented.
  • A JVM started with -XX:-UsePerfData is instrumented roughly a minute after it starts instead of immediately. Some database images, such as Cassandra, set this by default.

Admission-Time Injection

How It Works

When a pod is created, the operator adds three things to it:

  1. A shared volume, and an init container that copies the Java agent into that volume.

  2. A read-only mount of that volume in every container in the pod.

  3. The agent's -javaagent option, merged into each container's JAVA_TOOL_OPTIONS environment variable. Any value your application already sets is preserved.

The agent is therefore present before the JVM starts, and every connection the application opens is instrumented.

Injection happens only when a pod is created. Pods that already exist are not modified, so roll out your workloads after enabling it. Injection is also idempotent: a pod that already carries the agent is left alone.

Workload Opt-In Annotation

No workload is injected unless it opts in. Set this annotation to true on a pod, or on a namespace to opt in every pod within it:

instrumentation.attrb.io/inject-java: "true"

On a Deployment, the annotation belongs on the pod template, not on the Deployment itself:

spec:
template:
metadata:
annotations:
instrumentation.attrb.io/inject-java: "true"

Enabling Injection

sensorDisableAutoJavaInstrumentation: true
javaAutoInject:
enabled: true
mode: auto

Selecting a Backend

Two interchangeable backends perform identical injection. mode selects between them, and the default auto picks the right one for your cluster:

modeBackendCluster requirement
autoChooses policy on Kubernetes 1.36 and above, otherwise webhookAny supported version
policyRuns inside the Kubernetes API serverKubernetes 1.36 or above
webhookA small admission service, deployed by the chartKubernetes 1.28 or above

Leave this at auto unless you have a specific reason to pin a backend. Setting a backend your cluster cannot support fails the installation with an explanatory message rather than installing something that does not work.

The policy backend is evaluated by the API server itself, so it adds no workload to your cluster. The webhook backend runs as a small, highly available Deployment that the API server calls when a pod is created; the chart provisions it, along with its own certificates, service, and permissions, with no further setup on your part.

Configuration Reference

ValueDefaultDescription
javaAutoInject.enabledSet in your values fileEnables admission-time injection.
javaAutoInject.modeautoBackend selection: auto, policy, or webhook.
javaAutoInject.failurePolicyIgnoreIgnore lets pods start uninstrumented if injection fails. Fail blocks pod creation instead.
javaAutoInject.injectAnnotationinstrumentation.attrb.io/inject-javaThe opt-in annotation.
javaAutoInject.image.repositoryquay.io/attribute/java-agentImage containing the agent.
javaAutoInject.image.tag0.0.11Agent image tag.
javaAutoInject.mountPath/attrb-jagentWhere the agent volume is mounted in your containers.
javaAutoInject.resourceNameattrb-jagentName of the injected volume and init container.

Keep failurePolicy at Ignore in production. It means a problem with injection leaves an application running without instrumentation, rather than preventing it from starting.

Advanced Webhook Tuning

These apply to the webhook backend only and can be left at their defaults. The most useful one is namespaceSelector.

ValueDefaultDescription
javaAutoInject.webhook.namespaceSelector{} (all namespaces)Restricts which namespaces are sent to the webhook at all. Recommended on large clusters.
javaAutoInject.webhook.replicas3Replica count for the webhook Deployment.
javaAutoInject.webhook.timeoutSeconds2How long the API server waits for the webhook before applying failurePolicy.
javaAutoInject.webhook.antiAffinitypreferredSpreads replicas across nodes. required guarantees spreading but leaves replicas pending if there are too few nodes.
javaAutoInject.webhook.resources50m / 64Mi requestsRequests and limits for the webhook pods.

Every pod created in a selected namespace is sent to the webhook, so on a large cluster narrowing namespaceSelector to the namespaces that actually use injection is the most effective tuning available:

javaAutoInject:
webhook:
namespaceSelector:
matchLabels:
attrb.io/inject-java: "true"

Your Attribute release namespace and kube-system are always excluded, whatever you configure here.

Note

If you run the chart with networkPolicy.enabled=true, the webhook backend opens its own port so the API server can reach it. All other Attribute components remain subject to the default-deny policy.

Post-Installation Validation

For dynamic instrumentation, confirm the agent reached a Java application container and was loaded by the JVM. The first file shows the sensor delivered the agent; the second is written by the agent itself once it starts, so it confirms the JVM accepted it:

kubectl exec <java-pod> -- ls /tmp/jagent.jar /tmp/attrb_agent.jar

For admission-time injection, confirm a newly created pod carries the agent. Both commands should return a value:

kubectl get pod <java-pod> -o jsonpath='{.spec.initContainers[*].name}'
kubectl get pod <java-pod> -o jsonpath='{.spec.containers[0].env[?(@.name=="JAVA_TOOL_OPTIONS")].value}'

In webhook mode, confirm the webhook itself is available:

kubectl get pods -n attribute -l app=attrb-inject-java-webhook

Troubleshooting

Instrumentation is designed to fail quietly: if something prevents it, your application starts and runs normally without it. This is the safe behavior, but it means a missing agent produces no obvious symptom. Work through the table below.

SymptomLikely causeWhat to do
No instrumentation, admission-time injectionThe pod predates the configuration changeRoll out the workload. Only newly created pods are injected.
No instrumentation, admission-time injectionMissing opt-in annotationConfirm the annotation is on the pod template, not the Deployment, or on the namespace.
No instrumentation, admission-time injectionInjection is failing silently under failurePolicy: IgnoreTemporarily set javaAutoInject.failurePolicy=Fail. Pod creation then reports the underlying error. Revert afterwards.
No instrumentation, dynamic modeApplication container has a read-only /tmpUse admission-time injection for this workload.
No instrumentation, dynamic modeJVM runs with -XX:+DisableAttachMechanismRemove the flag, or use admission-time injection.
Instrumentation appears about a minute late, dynamic modeJVM runs with -XX:-UsePerfDataExpected. Use admission-time injection if you need coverage from startup.
Some connections attributed, others not, dynamic modeConnections opened before the agent attachedRestart the application, or use admission-time injection.
Nothing at all, any modeUnsupported JVM, Java version, or architectureCheck the application against the requirements above.
Installation fails with a message about sensorDisableAutoJavaInstrumentationBoth modes were enabled togetherSet sensorDisableAutoJavaInstrumentation: true when using admission-time injection.

For a deeper look in dynamic mode, enable debug logging on the sensor and check its logs for Java attach activity. Attach failures are always logged; successful attaches are logged at debug level only. Contact Attribute support if you need help interpreting the output.

Files Added to Your Containers

Both modes place files inside your application containers. This is what to expect.

Dynamic instrumentation writes the agent and a small attach helper into the application container's /tmp directory, then attaches the agent to the running JVM. These files remain in the container's writable layer for the lifetime of the container and are removed when it is replaced. Account for a few tens of megabytes of ephemeral storage per instrumented container if you monitor it closely.

Admission-time injection places the agent in a dedicated volume that the operator adds to the pod. Nothing is written into your container images or their writable layers, and everything is removed with the pod.

In both cases the agent runs inside your application's JVM in order to observe its connections. It is added to the running application only; your container images are never modified.


Trademarks: Java and GraalVM are registered trademarks of Oracle and/or its affiliates. Kubernetes is a registered trademark of The Linux Foundation. Amazon Corretto is a trademark of Amazon.com, Inc. or its affiliates. Eclipse and Temurin are trademarks of the Eclipse Foundation. Azul and Zulu are trademarks of Azul Systems, Inc. All other trademarks are the property of their respective owners.