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.
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-64orarm64. - 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
/tmpdirectory 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 Instrumentation | Admission-Time Injection | |
|---|---|---|
| Applies to | Every supported JVM on every node | Only workloads you opt in |
| Requires redeployment | No | Yes — pods must be recreated |
| Connection coverage | Connections opened after the agent attaches | All connections, from application start |
| Kubernetes version | 1.28 or above | 1.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.
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:+DisableAttachMechanismcannot be instrumented. - A JVM started with
-XX:-UsePerfDatais 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:
-
A shared volume, and an init container that copies the Java agent into that volume.
-
A read-only mount of that volume in every container in the pod.
-
The agent's
-javaagentoption, merged into each container'sJAVA_TOOL_OPTIONSenvironment 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:
mode | Backend | Cluster requirement |
|---|---|---|
auto | Chooses policy on Kubernetes 1.36 and above, otherwise webhook | Any supported version |
policy | Runs inside the Kubernetes API server | Kubernetes 1.36 or above |
webhook | A small admission service, deployed by the chart | Kubernetes 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
| Value | Default | Description |
|---|---|---|
javaAutoInject.enabled | Set in your values file | Enables admission-time injection. |
javaAutoInject.mode | auto | Backend selection: auto, policy, or webhook. |
javaAutoInject.failurePolicy | Ignore | Ignore lets pods start uninstrumented if injection fails. Fail blocks pod creation instead. |
javaAutoInject.injectAnnotation | instrumentation.attrb.io/inject-java | The opt-in annotation. |
javaAutoInject.image.repository | quay.io/attribute/java-agent | Image containing the agent. |
javaAutoInject.image.tag | 0.0.11 | Agent image tag. |
javaAutoInject.mountPath | /attrb-jagent | Where the agent volume is mounted in your containers. |
javaAutoInject.resourceName | attrb-jagent | Name 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.
| Value | Default | Description |
|---|---|---|
javaAutoInject.webhook.namespaceSelector | {} (all namespaces) | Restricts which namespaces are sent to the webhook at all. Recommended on large clusters. |
javaAutoInject.webhook.replicas | 3 | Replica count for the webhook Deployment. |
javaAutoInject.webhook.timeoutSeconds | 2 | How long the API server waits for the webhook before applying failurePolicy. |
javaAutoInject.webhook.antiAffinity | preferred | Spreads replicas across nodes. required guarantees spreading but leaves replicas pending if there are too few nodes. |
javaAutoInject.webhook.resources | 50m / 64Mi requests | Requests 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.
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.
| Symptom | Likely cause | What to do |
|---|---|---|
| No instrumentation, admission-time injection | The pod predates the configuration change | Roll out the workload. Only newly created pods are injected. |
| No instrumentation, admission-time injection | Missing opt-in annotation | Confirm the annotation is on the pod template, not the Deployment, or on the namespace. |
| No instrumentation, admission-time injection | Injection is failing silently under failurePolicy: Ignore | Temporarily set javaAutoInject.failurePolicy=Fail. Pod creation then reports the underlying error. Revert afterwards. |
| No instrumentation, dynamic mode | Application container has a read-only /tmp | Use admission-time injection for this workload. |
| No instrumentation, dynamic mode | JVM runs with -XX:+DisableAttachMechanism | Remove the flag, or use admission-time injection. |
| Instrumentation appears about a minute late, dynamic mode | JVM runs with -XX:-UsePerfData | Expected. Use admission-time injection if you need coverage from startup. |
| Some connections attributed, others not, dynamic mode | Connections opened before the agent attached | Restart the application, or use admission-time injection. |
| Nothing at all, any mode | Unsupported JVM, Java version, or architecture | Check the application against the requirements above. |
Installation fails with a message about sensorDisableAutoJavaInstrumentation | Both modes were enabled together | Set 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.