Security Considerations for Cross-Service Baggage in OpenTelemetry
What can go wrong when small notes travel with every request through every service, and the practical defenses that keep them safe, cheap, and trustworthy.
Imagine a parcel that passes through twenty pairs of hands on its way to a customer. Now imagine that anyone, at any step, can stick a small note on the parcel, and that every later handler reads the notes and sometimes acts on them. Most of the time this is handy: “fragile”, “deliver before noon”, “customer is on the premium plan”. But what if a stranger stuck a note on the parcel before it ever entered your warehouse? What if one note said “customer’s home address and phone number”, and the parcel was then handed to an outside courier who photographs every label?
This is the situation with baggage in OpenTelemetry. Baggage is a way of attaching small pieces of information to a request so that every service it touches can read them. It is useful, and it is also a quiet source of security trouble, because it travels automatically, it is not checked for honesty, and it is easy to forget that it is there. This guide explains, step by step and in plain words, what the risks are and how to build a system where baggage stays helpful and harmless.
1What Baggage Is, and Why Security Matters
Before talking about danger, we need to know exactly what we are protecting.
The simple definition
Baggage is a set of name and value pairs that travels along with a request as it moves from service to service. For example, the pair customer.tier with the value premium can be set once at the front door. Every service that handles the request afterward can read it, without having to ask a database or call another service. Baggage is part of OpenTelemetry’s idea of context, the bundle of facts that follows a request through a system, which also carries the trace identifiers that link steps together.
Think of the paper tag tied to a suitcase at an airport. The tag is attached at check-in, and every worker along the way, from the loader to the transfer desk, can read it. Baggage in OpenTelemetry works the same way: a tag tied on at the start and read by everyone along the route. And just like a real suitcase tag, anyone who sees the suitcase can read the tag.
Why teams like it
Baggage solves a real problem. Suppose a request starts at a web server and passes through ten services. Only the first knows which experiment group the user belongs to. Without baggage, each of the other nine would need to look it up, or every developer would need to pass the value through every function call and every message by hand. With baggage, the first service sets it once and the framework carries it along. Common uses include experiment groups, region names, request priorities, plan tiers, and flags that help with debugging.
Why it needs special care
Four properties make baggage riskier than most other request data.
It travels automatically
Once set, it is attached to outgoing calls by the instrumentation, often without the developer thinking about it again.
It goes everywhere
Every service, proxy, and sometimes every outside partner on the route receives it.
It is not checked
There is no built-in way to prove that a value is honest or that it was not changed on the way.
It is plain text
The pairs are written in readable form in headers or message fields. Secure connections protect the wire, but not the places where headers get logged or displayed.
A good mental rule to keep through this whole guide: baggage is a postcard, not a sealed letter. Anyone who handles it can read it, anyone who writes on it can change it, and it should carry only things you would be comfortable having read aloud.
2How Baggage Travels
To defend a road, you must know where it goes. Here is the route baggage takes.
Inside one service: the context
Within a single running program, baggage lives in the context, a small container that is passed along with the current piece of work. Code can read entries from it, and can create a new version with extra entries. The baggage itself is never changed in place. Adding an entry produces a fresh copy, so earlier holders of the old copy are not surprised.
Between services: the propagator
When a service makes a call to another service, a component called a propagator writes the baggage into the outgoing message. For web calls, it appears as a header named baggage. The header is a list of entries separated by commas, each written as a name, an equals sign, and a value, with optional extra properties after a semicolon. Values are percent-encoded so that special characters survive the trip. On the receiving side, a matching propagator reads the header and rebuilds the baggage in the new service’s context.
Propagators are usually switched on for you. In many OpenTelemetry setups, the default configuration propagates both the trace context and baggage. Instrumentation libraries for web servers, web clients, remote procedure calls, and message systems call the propagators automatically. So a developer can set an entry in one place and, without writing any more code, see it appear in dozens of services.
flowchart LR
CL["Client"] -->|"baggage header"| GW["Edge gateway"]
GW -->|"baggage header"| A["Service A"]
A -->|"baggage header"| B["Service B"]
A -->|"baggage header"| C["Service C"]
A -->|"message attributes"| Q["Message queue"]
Q --> W["Background worker"]
B -->|"baggage header"| EXT["Outside partner API"]
Figure 1 shows how far the tags can travel. The client sends a request to the edge gateway. The gateway forwards it to Service A, which calls B and C, puts a message on a queue for a background worker, and finally B contacts an outside partner. At every arrow, the baggage may be written out again. Notice that two of those arrows, to the queue and to the outside partner, leave the part of the system you fully control.
Other carriers: remote procedure calls and messages
Web headers are only one carrier. Remote procedure call systems carry baggage in call metadata, which works much like headers. Message systems carry it in message headers or attributes, so it is stored with the message. Some environments, such as serverless functions or batch schedulers, pass context through event fields or environment settings. The names and shapes differ, but the idea is the same everywhere: a small set of text entries rides alongside the real data, written by one side and read by the other. Whenever you meet a new carrier, ask the same three questions. Who can write to it? Who can read it? How long does it stay?
Limits on size
The standard that defines the baggage header sets minimum limits that every implementation should support, in the range of about sixty-four entries, a few thousand bytes for each entry, and roughly eight thousand bytes in total. Implementations may drop entries that go over their limits. Web servers and proxies also have their own caps on the total size of headers, commonly somewhere between eight and sixteen thousand bytes, though this varies. Check the current specification and your own infrastructure for exact numbers. These limits matter for security, as Chapter 7 shows.
Because propagation is automatic, the person who adds an entry is rarely the person who knows everywhere it will go. Any security review of baggage must look at the whole route, not just the service that sets the value.
3Baggage Is Not Telemetry, and Where It Ends Up
One of the most common confusions is thinking that baggage automatically becomes part of traces, logs, and metrics. It does not, but it often gets there anyway.
On its own, baggage is only context. It is not written onto spans, log lines, or metric points. This is good news, because it means that putting an entry in baggage does not by itself send the value to your tracing vendor. But there are several ways it ends up in places you may not expect.
| Place | How baggage gets there | Who can then see it |
|---|---|---|
| Network headers between services | Propagators write it on every outgoing call | Every service, proxy, and anyone who can read headers |
| Proxy and gateway access logs | Logs configured to record all headers or the baggage header | Operators, support staff, log analysis tools |
| Span attributes | A baggage span processor, or custom code, copies entries onto spans | Everyone with access to the tracing system |
| Log lines | Logging libraries that add context fields automatically | Everyone with access to the logs |
| Metric labels | Custom code or views that use baggage as a label | Everyone with access to the metrics, plus the bill |
| Message queues and stored messages | Messaging instrumentation copies baggage into message headers | Anyone who can read or replay the queue, for as long as messages stay |
| Outside partners | Outgoing calls to partner services carry the header | The partner and everything the partner does with its logs |
| Browser tools | Browser instrumentation that attaches baggage to requests | The user of the browser, in developer tools |
Two surfaces to protect
It helps to picture two surfaces. The wire surface is everything that moves between systems: headers, message fields, and the logs of devices in between. The telemetry surface is everything that is stored for later: spans, logs, and metrics that received a copy of a baggage value. A good defense watches both. Closing one door while leaving the other open gives only the feeling of safety.
A letter can be read while it is in the post, and it can be read again after it is filed in a cabinet. Protecting the envelope is not enough if the filing cabinet is open to the whole office. Baggage has both the post and the cabinet.
Make a short list for your system of every place that reads baggage and every place that copies it. If nobody can produce that list, you have found your first task.
4A Plain-Words Threat Model
A threat model is simply an honest answer to three questions: what are we protecting, who might hurt it, and how?
What we are protecting
- Private information about customers, staff, and the company itself.
- Correct decisions, such as who may do what and how much each customer pays.
- Availability, meaning that systems stay up and fast.
- Budget, since telemetry that grows without limit costs money.
- Trust in the data, so dashboards and investigations are not misled.
Who might hurt it
Outside attacker
Anyone on the internet who can send a request, and therefore a baggage header.
Curious or careless insider
A developer or operator who adds sensitive values, or reads values they should not.
Compromised service
A service that an attacker has taken over and that can now change the baggage for everything downstream.
Partner or vendor
An outside party that receives your requests and may store or mishandle what they carry.
Honest mistake
No villain at all, just a shared library with a setting that nobody understood.
The trust zones
flowchart LR
subgraph U["Untrusted zone"]
C["Browsers and apps"]
P["Partner systems"]
end
subgraph E["Edge zone"]
G["Edge gateway"]
end
subgraph I["Internal zone"]
S1["Service A"]
S2["Service B"]
Q["Message queue"]
end
subgraph X["Outside vendors"]
V["Payment and analytics APIs"]
end
C -->|"baggage may be forged"| G
P -->|"baggage may be forged"| G
G -->|"cleaned baggage"| S1
S1 --> S2
S1 --> Q
S2 -->|"baggage may leak"| V
The picture in Figure 2 shows the most important idea in this guide. There are boundaries, and every boundary needs a guard. Incoming baggage from outside is untrusted input. Outgoing baggage to outside is a possible information leak. Inside, even trusted services can be wrong or compromised, so a little caution is still wise.
Three short attack stories
The free upgrade. A curious customer opens the browser’s developer tools and sees that a request includes a baggage entry about their plan. They change the value from “basic” to “premium” and resend the request. A service downstream sees “premium” and unlocks a feature. No password was stolen, and no bug in the usual sense was found. The system simply believed a note.
The loud neighbor. An attacker discovers that a baggage entry called “debug” makes services write detailed logs. They send it on every request. Log volume grows tenfold, storage fills, and the logging system slows down. Meanwhile, the extra detail also writes values from other customers’ requests into logs that a wider group can read.
The quiet leak. No attacker at all. A team adds a customer reference to baggage for tracing. A year later, a new feature calls an analytics vendor through a library that uses the same web client. For months the vendor receives the reference with every call. Nobody notices until an audit asks which outside companies hold customer references.
These stories share a pattern: the harm came from ordinary features working as designed, in places the designers had not pictured.
Six kinds of trouble in plain words
| Kind of trouble | What it means | Baggage example |
|---|---|---|
| Pretending | Someone claims to be something they are not | A client sends a baggage entry saying it is a premium customer |
| Tampering | Data is changed on its way | A compromised service rewrites the tenant entry |
| Denying | Actions cannot be traced to a person | Baggage used as the only record of who did something |
| Leaking | Private data reaches the wrong eyes | An email address sent to a partner API |
| Overloading | Systems slow or fail under too much data | An enormous baggage header repeated on every internal call |
| Overreaching | Someone gains more power than they should have | A baggage flag that switches on an admin path |
The next chapters take these one at a time, and add a few that are special to baggage, such as injection into other systems, noisy telemetry, and data that leaks between requests.
5Threat: Leakage of Sensitive Information
The most common baggage problem is not a clever attack. It is a helpful developer putting the wrong thing in the bag.
A made-up story
Imagine a company called Harbor Foods, a name invented for this guide. A support engineer wants to see the email address of the affected customer in every service’s logs, to speed up investigations. So the login service sets a baggage entry user.email. A week later, the checkout service calls an outside shipping partner. The instrumentation attaches the baggage header, as it does to every outgoing call. The partner’s servers now receive the customer’s email address with every request, whether or not the partner has any right to it, and the partner’s access logs keep it for ninety days. Nobody at Harbor Foods did anything on purpose. The leak was the sum of three reasonable decisions.
The ways leaks happen
- Outbound calls to third parties. Payment gateways, mapping services, analytics tools, and shipping partners are all just “downstream calls” to the instrumentation.
- Infrastructure logs. Load balancers, gateways, and web servers can record headers. Anything in a header may end up in a log that many people can search.
- Telemetry copies. When baggage is copied onto spans and log lines, the value is stored in the observability system, often with broad access and long retention.
- Error pages and debugging tools. Debug output and error reports sometimes print all headers.
- Browsers. If a browser application attaches baggage to requests, then the user, and anyone with access to their device, can read it. Browser applications also need the server to explicitly allow the baggage header across origins, and that permission should not be given to every site.
Writing a private fact in baggage is like writing it on a postcard that will be handed from person to person around a whole city. Even if every person is honest, the card will be seen by many, and one of them may photograph it or leave it on a bus.
Giving support teams what they need, safely
The usual reason people put private values in baggage is a good one: they want to find a customer’s requests quickly. There are safer ways to give support that power. Carry a short, random request reference that identifies the request but says nothing about the person. Store the link between that reference and the customer in a protected system, with access limited to support staff and a record of who looked. Support then searches by reference, and an engineer who reads a trace or a log sees only a meaningless code. If the reference leaks, little is lost, because it cannot be used without the protected link.
What counts as sensitive
| Kind of value | Examples | Should it be in baggage? |
|---|---|---|
| Secrets | Passwords, access tokens, session cookies, API keys | Never |
| Direct personal data | Names, email addresses, phone numbers, home addresses, government IDs | Never |
| Indirect personal data | Account numbers, device IDs, precise locations | Avoid. If needed, use an opaque, short-lived stand-in |
| Business-sensitive facts | Prices, discounts, internal risk scores, fraud flags | Avoid. Look them up inside where needed |
| Internal structure | Server names, internal URLs, database names | Avoid, because it helps attackers map your system |
| Harmless hints | Region code, experiment group, coarse plan tier, request priority | Yes, within size limits |
Even “harmless” values can be sensitive in combination. A region, a plan tier, and an experiment group may be harmless alone but together point to a small, identifiable group of people. Keep the list short and coarse.
6Threat: Forged Baggage and Misplaced Trust
If anyone can write on the tag, nobody should believe the tag when something important is at stake.
The core problem
The baggage header arrives from the network. Whoever sent the request decides what it says. There is no signature, no proof of origin, and no check that the entry matches reality. A service that reads customer.tier=premium has learned only that somebody claimed the customer is premium. If that somebody was a browser or a script, the claim is worthless.
Attacks that follow
- Privilege claims. An attacker sends an entry such as an admin flag or a tenant name, hoping a service will treat it as true.
- Free upgrades. An entry that changes pricing, limits, or features is a gift to anyone who guesses its name.
- Cost and load tricks. A “debug” or “verbose” flag that switches on expensive behavior lets an outsider make your systems work harder, or write more logs, at will.
- Skipping safeguards. A flag like “skip cache” or “skip checks”, added for convenience in testing, may still be honored in production.
- Tampering from inside. A compromised internal service can rewrite entries for everyone downstream, and the downstream services cannot tell.
- Guessable names. Attackers can try common names, since baggage names are visible in documentation, code, and traffic.
Imagine a nightclub where the doorperson lets in anyone who has a sticker saying “VIP” on their coat. Anyone can buy stickers at the corner shop. The sticker is useful for the staff to find regular guests quickly, but it must never be the only thing that opens the door.
The rule: hints, not decisions
The safe way to think is that baggage can carry hints, never decisions. A hint helps a service do something a little smarter, such as choosing which experiment text to show. A decision is something that must be right for the system to be secure or correct, such as who is allowed to see which data, or how much to charge. Decisions must be made from facts the service can verify: an authenticated identity, a verified token, or a record in a trusted database.
| Use of the value | Safe in baggage? | Better source |
|---|---|---|
| Choose experiment wording | Yes, a wrong value only changes a label | Baggage is fine |
| Add a tag to dashboards | Yes, if sanitized | Baggage is fine, with an allow list |
| Decide who may see data | No | Verified identity token checked by the service |
| Select the customer’s tenant | No | Tenant taken from the authenticated session at the edge |
| Set prices or limits | No | A trusted lookup by authenticated ID |
| Enable debug or skip safeguards | No | Operator controls with their own authentication |
Stale and replayed claims
Even an honest value can become false. A request captured today can be replayed tomorrow by anyone who saw it, carrying yesterday’s entries. A stored message may carry a plan tier that has changed. Because baggage has no timestamp and no expiry, a service cannot tell how old a claim is. Another reason to keep decisions out of baggage: the world changes, and the tag does not.
Making honest values more believable
The edge gateway should remove any baggage coming from untrusted clients and, where needed, set fresh values itself from facts it has verified. That way, inside the system, an entry that was set by the gateway can be trusted somewhat more than an entry that arrived from outside, because the outside entries were never allowed through. Where a value must be trusted by several services, the safest design is not baggage at all, but a signed token that carries the claim and that every service verifies.
7Threat: Size and Amplification
A single large header is a nuisance. The same header repeated across a hundred internal calls is an attack.
Why baggage can be used as a megaphone
An incoming request becomes many outgoing requests as it passes through a system. A page that looks simple to a user may cause dozens of internal calls. Each internal call carries the baggage header. If an attacker sends a large baggage header once, your own systems will copy it onto every call, multiplying its size across your internal network, your proxies, and your logs. The attacker spends a little. You spend a lot.
flowchart TD
ATK["One request with an 8 KB baggage header"] --> GW["Edge gateway"]
GW --> A["Service A"]
A --> L1["40 calls, each carrying 8 KB"]
L1 --> L2["Each call causes 3 more calls, 120 in total, each carrying 8 KB"]
A worked example
| Step | Calculation | Result |
|---|---|---|
| First level calls | 40 calls times 8 KB | 320 KB |
| Second level calls | 40 calls times 3 follow-up calls is 120 calls, times 8 KB | 960 KB |
| Extra traffic per attacker request | 320 KB plus 960 KB | About 1.25 MB |
| Attacker rate | 200 requests per second from a small botnet | 200 per second |
| Extra internal traffic | 1.25 MB times 200 | About 250 MB per second, around 2 gigabits per second |
The numbers are invented for teaching, but the shape is real. A small stream of cheap requests creates a heavy stream of internal traffic, plus extra work for parsing headers, writing logs, and, if the value is copied to telemetry, storing it. The attacker needs no special skill.
Trim, reject, or pass through?
When the edge meets an oversized or odd header, it has three choices. It can trim it, keeping only the allowed entries that fit. It can reject the whole request with an error. Or it can pass it through unchanged. Passing through is the unsafe choice and should be used almost never. Trimming is friendly to real users, because an honest client with one extra entry still gets served. Rejecting is better for clearly malformed or hostile input, because it gives the sender a clear signal and saves work. A common mix is to trim ordinary excess and reject headers that cannot be parsed or that exceed a much higher hard limit. Whatever you choose, count each outcome.
Other size problems
- Rejections. If the total headers grow past a proxy’s cap, requests may fail with errors. An oversized baggage header can therefore break real users’ requests, not just the attacker’s.
- Silent dropping. Implementations may silently drop entries above their limits, so important entries could disappear without any message.
- Memory. Many small entries cost parsing time and memory at every hop.
- Growth by accident. Even without attackers, teams may add entries over years until the header is big and nobody knows why.
The defense: strict, small limits at the edge
Choose your own limits, much smaller than the standard’s maximums, and enforce them where requests enter. A sample policy might allow no more than ten entries, no more than sixty-four characters per name and value, and no more than five hundred bytes in total. Anything over the limit is either trimmed to the allowed entries or removed whole. Then internal services enforce the same limits again when they add entries, so that a growing system cannot creep upward. With the policy above, the same attack from the worked example would add at most five hundred bytes per call instead of eight thousand, a sixteen-fold reduction, and would be easy to notice in monitoring.
Treat the baggage budget like a luggage allowance on a flight. A small, strict allowance forces everyone to think about what is truly worth carrying.
8Threat: Injection Into Downstream Systems
A baggage value is text that someone else chose. When your code pastes that text into something else, the writer gets a say in what happens.
Picture a restaurant where the waiter copies each guest’s note straight into the recipe card that the cook follows. If a guest writes “add salt”, fine. If a guest writes “and also give everyone at the next table a free dinner”, the cook may follow that too. Pasting untrusted text into instructions is how injection attacks work, and baggage values are untrusted text.
Where baggage values often get pasted
| Destination | What an attacker tries | The defense |
|---|---|---|
| Log lines | Includes line breaks to create fake log entries that look real, or hides evidence | Remove control characters and line breaks, and log values in a structured field, not pasted into a sentence |
| Database queries | Includes query fragments to read or change data | Always use parameterized queries and never build queries by joining text |
| Commands and scripts | Includes shell symbols to run extra commands | Never place baggage values in commands. If unavoidable, pass them as separate, escaped arguments |
| Web addresses and redirects | Points users or services to an attacker’s site | Allow only known destinations, never build addresses from raw values |
| Outgoing headers | Includes line breaks to add or change other headers | Reject values with control characters, and use libraries that encode safely |
| Web pages and admin tools | Includes script code that runs in an operator’s browser | Escape values when displaying them, in dashboards, trace viewers, and error pages |
| File paths | Includes path tricks to reach other files | Never build file paths from values, or restrict them to a fixed list |
| Templates and expressions | Includes expression syntax that the engine evaluates | Pass values as data, never as part of the template |
Decode first, then validate
Baggage values are percent-encoded on the wire. A harmless-looking string such as “%0A” decodes into a line break. This means that a check done on the encoded text may miss what the decoded text contains. The right order is to decode once, then validate the decoded text against a strict pattern, and never decode twice.
Log forging in plain words
Logs are often read as a story, one line at a time. If a baggage value contains a line break followed by text that looks like a log line, the log now contains a line that nobody’s code wrote. An attacker can use this to invent events, such as a successful login, or to bury real events under noise. The remedy is simple. Write values into structured fields, so that the logging system stores them as data, and strip control characters before they get anywhere near a log.
Prefer small, fixed shapes
The safest values are ones that can be checked against a short list: “region equals one of five known codes”, “tier equals one of four known names”. Convert the incoming text into a fixed type, such as an enumeration, at the moment of reading. If the text does not match, treat the value as missing. This turns free text, which is dangerous, into a choice from a menu, which is not.
Give your teams one small shared helper for reading baggage. The helper returns typed, validated values and refuses everything else. When all reads go through it, a whole class of mistakes disappears at once.
9Threat: Telemetry Poisoning and Cardinality Explosions
Copying baggage onto spans, logs, and metrics is useful, and it hands an outsider the pen that writes in your observability tools.
Why teams copy baggage into telemetry
Baggage is a convenient way to label telemetry with business facts. If every span carries the plan tier, then engineers can ask “are premium customers slower?” Many SDKs offer a baggage span processor that copies entries onto every span. The idea is good. The risk is in which entries, and who chose their values.
Cardinality: the explosion of unique values
Cardinality means the number of different values that a label can take. Metric systems create a separate time series for every combination of label values. A label with five possible values is cheap. A label that an outsider can set to anything creates a new series for every request.
Take an example. Say five services each produce twenty metrics. If baggage copies a label that could take one million different values, the number of series could grow to five times twenty times one million, or one hundred million series. No metrics backend is built for that. Costs jump, queries slow, and the system may refuse more data or fall over. An attacker who knows that a label is copied can do this on purpose by sending random values. The same thing can also happen by accident when a developer adds a user ID to baggage.
Finding the attribute that is exploding
When costs or slowness appear, the cause is usually a single attribute. Ask your telemetry system for the number of distinct values per attribute over the last hour, sorted from highest to lowest. An attribute with thousands of values per hour that you expected to have five is the culprit. Trace it back to its source: a baggage copy rule, a library setting, or a new code change. Then remove the rule and add a limit so the problem cannot return quietly.
Other kinds of poisoning
- Skewed experiments. If an experiment group comes from baggage, an outsider can send fake groups, and your analysis counts them.
- Wrong cost reports. If cost labels come from baggage, a service can claim another team’s name and shift the bill.
- Misleading dashboards. Fake values can hide real problems, or create false alarms.
- Indexing load. Tracing backends that index attributes can be slowed by endless new attribute values or huge ones.
- Hidden messages. Long values can smuggle text, such as links, into tools that display attributes to staff.
Defenses
| Defense | How it helps |
|---|---|
| Copy only allow-listed keys | New or unexpected names are never copied, whoever invented them |
| Map values to a fixed set | Unknown values become “other”, so cardinality stays bounded |
| Never use baggage as a metric label unless it has a small fixed set of values | Prevents series explosions |
| Hash or bucket identifiers for traces only | Allows correlation without storing raw values, and keeps them out of metrics |
| Set attribute count and length limits in the SDK | Caps the harm from huge values |
| Remove unknown attributes in the Collector | A second line of defense if an application copies too much |
| Watch the number of distinct values | Early warning when something starts to grow |
The most dangerous setting is “copy all baggage onto every span”. It is easy to turn on and gives an outsider a way to put free text into every trace you store. Use a named list of allowed keys instead.
10Threat: Context Bleed Between Requests and Tenants
Sometimes the danger is not an attacker at all. It is a note that was left behind and read by the wrong person.
In a hotel, the housekeeper must clear the room completely between guests. If the previous guest’s sticky notes stay on the desk, the next guest reads private details. Context bleed is a housekeeping failure: the previous request’s baggage is still in the room when the next request arrives.
How the context follows the work
The current context is usually tied to the current thread or task. Frameworks switch the context on when a request begins and off when it ends. That works well as long as each piece of work runs entirely on its own. Trouble starts when work jumps between threads, is delayed, or is reused.
Common causes
- Thread pools and reused workers. If a worker picks up a task and the context from an earlier task is still attached, the new task runs with the wrong baggage.
- Scopes that never close. Code that makes a context current but forgets to detach it in all paths, especially on errors, leaves it behind.
- Async callbacks and futures. A callback may run later, in another thread, with either no context or a stale one, depending on how the framework carries it.
- Global or shared variables. A context stored in a shared place, such as a cache or a singleton, is visible to every request.
- Background jobs started from requests. A job that inherits the context of the request that started it keeps that baggage for its whole life, which may be hours.
- Connection reuse. Rarely, code stores request details on a long-lived connection object.
What it looks like
A support agent searching logs for customer A sees log lines labeled with customer B’s tenant. A background job for tenant A writes a record that is tagged for tenant B. A premium flag from a previous request switches on features for the next customer. These bugs are hard to find because they appear only under load, when threads are busy and reused.
Defenses
- Always close scopes. Use the language’s safe pattern so that the scope is released even when errors occur.
- Use context-aware executors. Wrap thread pools and schedulers with the helpers that carry context explicitly, and test them.
- Start clean. At the beginning of each incoming request, and at the start of each background job, begin with a fresh context.
- Keep context out of shared state. Never store baggage or context in global places.
- Do not use baggage for tenant identity. Tenant isolation should come from verified identity, so that a bleed cannot cause a cross-tenant leak of real data.
- Test under concurrency. Run tests that fire many parallel requests with different baggage values, and assert that each request only ever sees its own.
11Threat: Queues, Retries, and Data That Lives Too Long
A web call is over in a moment. A message in a queue can sit for days, and baggage sits with it.
flowchart LR
P["Producer service"] -->|"baggage copied into message headers"| Q["Queue stores messages for days"]
Q --> C1["Consumer reads soon"]
Q --> DLQ["Dead letter queue read by operators"]
Q --> MIR["Mirror to analytics or a partner"]
Q --> C2["Consumer reads much later with stale values"]
What changes with messages
Messaging instrumentation usually copies the context, including baggage, into the message’s headers or attributes. That turns a short-lived tag into stored data. It is now written to disk, copied to replicas, backed up, and visible to everyone who can read the queue. It may also be moved to places you did not plan.
An example of the slow burn
Suppose an order service puts a message on a queue and a shipping service reads it two days later because of a backlog. The message carries baggage from the original request, including a flag that said “expedited” at the time. The customer cancelled the expedited option an hour later, but the flag lives on. The shipping service honors it and the company pays for faster delivery nobody wanted. Nothing was hacked. The system simply believed an old note.
Specific risks
- Stale claims. A message created when a customer was on a premium plan may be processed after the plan ended, and the baggage still says premium.
- Replays and retries. Failed messages are retried and sent to dead-letter queues, where operators or tools may inspect them and see every baggage entry.
- Mirroring and exports. Messages are often copied to analytics systems, other regions, and partners, taking baggage with them.
- Restored backups. Restoring data into a test environment may bring production baggage with it.
- Batches. A consumer that reads fifty messages at once has fifty different baggage sets. Which one applies to the batch? Using any single one is a mistake.
- Limits. Messaging systems cap how many attributes a message can carry. Baggage may squeeze out other, more important attributes.
Defenses
- Decide on purpose. By default, do not copy baggage into messages. Allow only specific keys, on specific topics, where there is a clear need.
- Prefer explicit fields. If the consumer needs a fact, put it in the message body with the same access controls as the rest of the message, and have the consumer re-check anything that matters.
- Treat stored baggage as old. Never use values from messages to make decisions without re-verification.
- Use links for batches. When a consumer processes many messages together, record links to each message’s trace, rather than adopting one parent context and its baggage.
- Keep retention short for queues and dead-letter queues, and restrict who may read them.
- Scrub before copying data out. When messages go to analytics, partners, or test systems, remove baggage first.
12Privacy and Compliance
Baggage is data about people and businesses. The rules that apply to other data apply to it too.
Privacy laws and company policies are written about personal data, which means any information that can identify a person directly or indirectly. They rarely care which technical mechanism carries the data. If baggage includes a customer number, a device ID, a precise location, or anything that could be linked back to a person, then baggage is carrying personal data, and the usual duties apply. This guide cannot give legal advice, and rules differ by country and industry, so involve your privacy and legal teams. The principles below are the common ground.
| Principle | What it means | What it means for baggage |
|---|---|---|
| Minimize | Collect and share only what is needed | Carry no personal data unless truly necessary, and the least detail possible |
| Limit the purpose | Use data only for the reason it was collected | A flag set for experiments should not be copied into marketing logs |
| Limit the audience | Share only with those who need it | No automatic sharing with partners, strip at the exit |
| Limit the time | Keep data no longer than needed | Short retention in queues, logs, and traces that hold copies |
| Know where it is | Keep an inventory of data flows | A registry of baggage keys and where each one goes |
| Honor requests | Support access and deletion requests | If a person’s data is in telemetry, you must be able to find and remove it |
| Respect borders | Some data may not leave certain regions | Baggage carried to a service in another region is a transfer |
Retention and deletion in practice
People may ask a company to delete the personal data it holds about them. If baggage values have been copied into logs and traces, deletion becomes a search through many systems, each with its own storage and backups. The easy cure is prevention: if no personal data was ever stored there, there is nothing to delete. If some was stored, shorter retention in telemetry stores limits how long the problem lasts, and a registry of where each key was copied tells you where to look.
The simplest path
The easiest way to comply is to keep personal data out of baggage completely. If a service really needs to know something about the user, it can look it up from a trusted store using the verified identity, with all the access control and record keeping that store already has. This keeps baggage in the clear-cut category of “harmless hints”, and keeps your list of personal data flows short and easy to explain.
If someone asks, “Does our tracing system hold personal data?”, you want the answer to be a quick, confident “no, and here is how we make sure”, not a week of searching.
13What May Go Into Baggage
Instead of reacting to every threat one by one, ask four simple questions before any entry is allowed.
flowchart TD
Q1{"Would it hurt if a stranger read it"} -->|"yes"| N1["Do not put it in baggage"]
Q1 -->|"no"| Q2{"Will any decision depend on it being true"}
Q2 -->|"yes"| N2["Use a verified token or a trusted lookup"]
Q2 -->|"no"| Q3{"Is it small, with a fixed set of values"}
Q3 -->|"no"| N3["Use a short opaque ID or leave it out"]
Q3 -->|"yes"| Q4{"Must it reach outside partners"}
Q4 -->|"yes"| N4["Send it as an explicit field in the partner interface"]
Q4 -->|"no"| OK["Allowed in baggage. Add it to the registry"]
Reading the flow
First question: would it hurt if a stranger read it? Secrets, personal data, and internal structure fail at once. Second: will any decision depend on it being true? If yes, baggage is the wrong tool, because it cannot be trusted. Third: is it small and from a fixed set? That keeps size and cardinality under control, and makes validation easy. Fourth: must it reach outside partners? If so, do not rely on automatic header propagation. Send it deliberately, as a documented field that the partner agrees to receive.
The gray areas
Some entries are neither clearly safe nor clearly dangerous. A coarse location such as a country is usually fine, but a city or postcode may identify a small group. A plan tier is fine, but a plan tier plus an exact signup date may point to one person. An experiment cohort is fine, but a cohort name that reveals a product not yet announced is a business secret. When in doubt, move up one level of coarseness, or leave the entry out. It is far easier to add an entry later than to withdraw one that has already spread.
Good and bad entries
| Proposed entry | Verdict | Why |
|---|---|---|
| Region code with five possible values | Allowed | Small, fixed, harmless |
| Experiment cohort name | Allowed | Small, fixed set, only a hint |
| Request priority: low, normal, high | Allowed with care | Must not let outsiders jump the queue; set at the edge |
| User email address | Refused | Personal data, fails the first question |
| Session token | Refused | Secret |
| Tenant ID used to pick the database | Refused | A decision depends on it; use verified identity |
| Free text “support note” | Refused | Unbounded, injectable, possibly sensitive |
| Debug mode switch | Refused | Outsiders could switch on expensive behavior |
14Defense at the Entrance: The Edge Gateway
The cheapest place to stop forged or oversized baggage is the door, before it enters a thousand hallways.
flowchart LR
NET["Internet"] --> WAF["Web application firewall with header size rules"]
WAF --> EDGE["Edge gateway: strip, allow list, set trusted values"]
EDGE --> MESH["Internal services with a validation helper"]
MESH --> EGR["Egress proxy: remove baggage for outside hosts"]
EGR --> OUT["Outside partners"]
MESH --> OBS["Telemetry with allow listed baggage copies"]
MESH --> MQ["Queues with allow listed baggage only"]
What the gateway should do
| Action | Plain-words purpose |
|---|---|
| Strip by default | Remove the baggage header from every request that comes from an untrusted source. Most real clients have no legitimate reason to send it |
| Allow-list for known partners | If a trusted partner must send some entries, accept only the named keys, from that partner’s verified identity |
| Limit size and count | Cap the number of entries and bytes, much lower than the standard’s maximum |
| Validate form | Reject malformed headers, bad characters, and values that do not match the expected pattern |
| Set trusted values | Add entries yourself from verified facts, such as the region or the plan tier looked up from the authenticated identity |
| Count and report | Keep counters of what was stripped or rejected, without recording the values themselves |
| Handle browsers carefully | Do not allow arbitrary websites to send the baggage header across origins, and consider whether your own web app needs to send it at all |
Where to do it
The same job can be done by a reverse proxy, an API gateway, a service mesh ingress, a content delivery network’s edge functions, or a small piece of gateway code. Most of these can remove or overwrite request headers by rule. Choose the layer that already sits in front of all your traffic, so there is no side door. If different entry points exist, such as a public gateway and a partner gateway, each needs its own policy, because the trust level differs.
Partner traffic needs its own door
Some businesses exchange requests with partners who legitimately need to send or receive a few context entries, for example a shared correlation hint. Treat such partners as a separate trust zone with a separate entrance, not as the general public. Identify them strongly, for example with client certificates. Write down the exact keys that may be accepted from each partner, and the exact keys that may be sent to each partner. Reject everything else. Review the list on a schedule, because partnerships and their needs change.
Do the same for the trace context
The same reasoning protects the trace identifiers and the sampling flag that travel beside baggage. Outsiders who can set those can also force expensive sampling or confuse traces. A gateway that starts fresh trace context for untrusted callers closes both holes with one rule.
Be sure the stripping happens before any instrumentation reads the header. If a service reads the baggage first and the rule that removes it runs afterward, the damage is already done in that service’s context.
15Defense Inside: Validation and Safe Use
The edge is the first guard, not the only one. Services should still be careful with what they read and what they write.
Why inside defenses still matter
Edge rules can have gaps. A side entrance may exist, a partner may be allowed some entries, an internal service may be compromised, or a rule may fail to deploy. If every internal service trusts every entry just because “the edge checks everything”, then one gap becomes a full failure. Layered defense means each service keeps its own small guard.
One shared helper for every read and write
The most useful single control is a small shared library, owned by one team, which all services use instead of calling the raw baggage functions. It does the careful work in one place.
Typed and validated
Returns a value only if the key is on the allow list and the value fits the expected shape, such as one of five region codes. Otherwise it returns “missing”.
Budgeted
Refuses to add keys that are not in the registry, and refuses when the total size would pass the limit.
Cleaned
Removes control characters and truncates long values before they can reach logs or other systems.
Counted
Counts rejected reads and writes by key name, giving monitoring something to watch.
Make the safe way the easy way
- Lint rules. A rule in the build that flags direct use of the raw baggage functions outside the shared helper, with a short explanation of what to use instead.
- Namespaced keys. Names start with a short owner prefix, so that two teams cannot accidentally use the same key for different meanings, and so that readers know whom to ask.
- Code review checklists. Any change that adds a new key or a new copy of baggage into telemetry gets a security and privacy look.
- Known caller identity. When services authenticate to each other, for example with mutual certificates from a service mesh, a service knows who sent a request. Entries then become “claims made by service X”, which is somewhat more reliable than anonymous text, though still not proof of truth.
A review checklist for a new key
- Does the purpose fit in one sentence, and does someone own the key?
- Does it pass the four questions of Chapter 13?
- What is the exact set of allowed values, or the exact pattern and maximum length?
- Who sets it, and who reads it, and what does each reader do with it?
- Will it reach any outside party, any queue, or any stored telemetry?
- What happens if the value is missing, wrong, or hostile? The answer should be “nothing important”.
- When will the owner review whether the key is still needed?
Who is allowed to set what
| Key kind | Who may set it | What others do with it |
|---|---|---|
| Edge-set keys, such as region and plan tier | Only the edge gateway | Read as hints. Drop any copy that arrives from a source other than the edge |
| Team keys, such as experiment cohort | The owning team’s service | Read within the allowed purpose only |
| Anything else | No one | Ignored and counted as unknown |
Services that do not need baggage at all can turn the baggage propagator off in their configuration, so they neither read nor forward it. Be careful, though: if such a service sits in the middle of a call chain, downstream services that need the entries will no longer receive them.
16Defense at the Exit: Egress Control
The last guard stands at the door that leads out, making sure nothing leaves that should stay inside.
The main rule
Do not send baggage to outside parties unless there is a written reason. Outside parties include payment gateways, mapping and messaging providers, analytics tools, advertising networks, and every software-as-a-service product that your code calls. They did not ask for your internal hints, they may log them, and you may be unable to make them delete them later.
Where leaks hide
- Vendor libraries. A payment company’s client library uses an ordinary web client, which the instrumentation wraps, so the baggage header goes along.
- Webhooks and callbacks. Calls from your system to a customer’s server carry the header too.
- Redirects. A redirect that moves a call from your domain to another may carry headers across.
- Health checks and probes that call external monitoring services.
Ways to stop it
| Control | How it works | Strength | Weakness |
|---|---|---|---|
| Filtering propagator | A small wrapper that writes only allow-listed keys, or none, when the destination is not an internal host | Close to the code, works anywhere the code runs | Must be adopted by every service |
| Per-client configuration | Clients that call outside hosts are created without the propagator, or with instrumentation settings that skip them | Simple for known clients | Easy to forget for new clients |
| Egress proxy | All outbound traffic goes through a proxy that removes the baggage header for any destination not on an internal list | One central rule, catches every library | Needs good network design so that there is no bypass |
| Mesh egress gateway | A service mesh sends outside calls through a controlled exit that applies header rules | Fits clusters already using a mesh | Adds components to run |
| Domain allow list | Network controls that allow calls only to approved outside names | Limits where anything can go | Does not itself remove headers |
Using two of these together is wise. The proxy catches what the code forgets, and the code-level control protects when a service runs in a place without the proxy, such as a developer’s laptop or a new environment.
When a partner needs data
If a partner truly needs a fact that you currently keep in baggage, give it to them on purpose. Add a documented field to the partner interface, send only that field, describe it in the contract, and agree on how long they keep it. That turns an accidental leak into a deliberate, reviewable data share.
Outbound filtering that checks the destination by name can be fooled by internal names that point outside, such as a company domain that is really a vendor’s server. Keep the list of internal destinations exact and review it regularly.
17Controlling Baggage Inside Telemetry
Once a value is copied from baggage into a stored span, log, or metric, it lives as long as that data does. Choose carefully what to copy.
Copy with intent
Instead of “copy everything”, define a short list of baggage keys that are useful for analysis and safe to store. Only those are copied, to spans and logs. Everything else is left behind in the context. Each key on the list should have an owner and a reason in the registry.
Label the origin
A useful habit is to place copied values under a clear name prefix, so that anyone reading a trace can tell at a glance that the value came from baggage and was not measured or verified by the service. For example, a name such as baggage.region tells an analyst to treat the value as a hint. This simple labeling prevents the mistake of treating a forged flag as a fact in an investigation.
Where each signal stands
| Signal | May baggage be copied? | Conditions |
|---|---|---|
| Spans | Yes, for allow-listed keys | Short values, fixed sets, origin prefix, attribute limits set |
| Logs | Yes, for allow-listed keys | Sanitized values, no personal data, structured fields |
| Metrics | Only for enumerations with a small fixed set | Never for identifiers or free text, because cardinality explodes |
| Long-term archives | Avoid | Retention rules must match the most sensitive value stored |
Second line of defense: the Collector
Even when applications are careful, mistakes happen. The OpenTelemetry Collector can act as a safety net. Rules can remove attributes whose names are not on the approved list, mask values that match patterns such as email addresses, or delete anything that carries the origin prefix but is not registered. SDKs also have limits for the length of attribute values and the number of attributes per span, which can be set through configuration, such as OTEL_ATTRIBUTE_VALUE_LENGTH_LIMIT. Using both the SDK limits and the Collector rules gives two layers.
Access and retention
Telemetry stores are searched by many engineers. If baggage-derived values are stored there, apply the same access controls and retention rules as to any other data of that sensitivity. Review who can read raw traces and logs, and remove broad access that was granted just because “it is only debugging data”.
Think of a school notice board. Before pinning a note to it, you decide who will read it and for how long it will stay up. Copying baggage into telemetry is pinning notes on a public board, so the same questions apply.
18Governance: The Baggage Registry
Rules written on paper protect nothing unless someone owns them. A registry is the simplest tool for ownership.
What a registry is
A baggage registry is a list, kept in version control, of every key allowed in your systems. Each key has a row that answers the questions a security reviewer would ask. The registry also feeds the code: allow lists in the edge gateway, the shared helper, the telemetry copier, and the egress filter are all generated from it, so that there is one source of truth.
| Field | Question it answers |
|---|---|
| Name | What is the key called, with its owner prefix? |
| Owner | Which team answers questions and reviews changes? |
| Purpose | Why does this exist, in one sentence? |
| Allowed values | What are the exact allowed values or the pattern? |
| Data class | Is it public, internal, or sensitive? |
| Who sets it | The edge only, or named services? |
| Who reads it | Which services use it, and for what? |
| May cross to outside parties | Yes or no, and under what agreement? |
| Copied to telemetry | Which signals, if any? |
| Crosses queues | Which topics, if any? |
| Review date | When must the owner confirm it is still needed? |
flowchart LR
A["Proposal with purpose and data class"] --> B["Security and privacy review"]
B -->|"approved"| C["Added to the registry and allow lists"]
B -->|"rejected"| X["Use another design"]
C --> D["Used in services and monitored"]
D --> E["Periodic review"]
E -->|"still needed"| D
E -->|"unused"| F["Retired and removed"]
The lifecycle in practice
A team that wants a new key opens a short proposal that fills in the registry row. Security and privacy reviewers check it with the four questions from Chapter 13. If approved, the registry change is merged, and generated allow lists roll out to the edge, the helper, and the telemetry rules. Monitoring then shows how often the key is seen. Every quarter, owners confirm their keys. Keys that nobody reads are retired, and the retirement is made real by removing them from the allow lists and the code. This habit keeps the baggage small, which is itself a security measure.
Start with an honest inventory. Spend one day listing every baggage key that exists today by scanning code and sampling traffic at the gateway. Most companies are surprised by the number of forgotten keys.
19Testing the Defenses
A guard who has never been tested is only a guess. These checks turn the guess into evidence.
| Test | How it works | Pass condition |
|---|---|---|
| Validator unit tests | Feed the shared helper good values, bad values, and edge cases | Only valid, allow-listed values are returned |
| Malformed header tests | Send random, broken, and extreme headers: missing equals signs, repeated keys, huge counts, strange characters, line breaks, double encoding | No crashes, no passing of bad content, bounded time and memory |
| Forgery tests at the edge | From outside, send forged flags such as premium or admin | The flags never reach internal services |
| Oversize tests | Send headers at and above the limits | Rejected or trimmed, consistently, with counters raised |
| Injection tests | Place attack text in values and watch logs, queries, and pages | Text appears only as harmless data |
| Leak tests with marker values | Set a unique, harmless-looking marker in baggage, then search logs, spans, queues, and outbound traffic for it | The marker appears only in approved places |
| Concurrency tests | Run many parallel requests with different values | Each request sees only its own |
| Egress tests | Make calls to a test server standing in for an outside partner | No baggage header arrives |
| Registry consistency tests | Compare code, allow lists, and registry in the build | No key exists in one place and not in the others |
What to automate
Manual checks fade away. Put the cheap tests into the build, so that every change runs them: validator tests, malformed header tests, registry consistency tests, and a check that the egress rule still removes the header. Run the heavier tests, such as marker searches across all stores and the concurrency tests under load, on a regular schedule, for example every night or every week, and send failures to the owning team. Keep the exercises with human attackers for the times when you add major features or change infrastructure.
The marker trick
The leak test deserves special attention. It uses a “canary” marker, a unique value such as a random string that cannot be confused with real data. You attach it to a test request and then search everything you can reach: gateway logs, application logs, tracing storage, queue contents, and the traffic that arrives at a stand-in for an outside server. Wherever the marker appears, a path exists. Compare the results with your registry. Any place that is not allowed is a finding. Run this regularly, not just once, because every deployment may change the paths.
Practice real incidents
At least once or twice a year, run a short exercise in which one team plays an attacker who tries to use baggage to gain a premium feature, leak data to a partner, or cause an overload. The second team defends using only monitoring and runbooks. These exercises reveal gaps in dashboards and in understanding that automated tests cannot.
20Monitoring and Responding
Defenses need eyes. Monitoring tells you when they are tested, when they fail, and when someone is probing them.
What to measure
| Place | Signal to watch | What a change means |
|---|---|---|
| Edge gateway | Requests with baggage, stripped, rejected, oversize, malformed | A jump signals scanning or a misconfigured client |
| Internal services | Distribution of baggage size and entry count, unknown keys seen, rejected reads and writes by key | Growth hints at creep or a new unreviewed key |
| Telemetry stores | Distinct values per copied attribute, attribute length, series counts | Rapid growth hints at cardinality abuse |
| Egress | Outbound requests to outside hosts that still carry the baggage header | Should be zero. Any count is an incident or a bypass |
| Queues | Messages carrying baggage, by topic | New topics carrying baggage need review |
| Errors | Requests refused because headers were too large | Real users may be harmed by oversize baggage |
Notice that most of these are counts and shapes, not values. Monitoring should never store the baggage values themselves, because that would copy the very data you are trying to protect into another system.
A simple dashboard
One page is enough to start. At the top, put one big number: baggage headers seen on traffic leaving to outside hosts, which should be zero. Below that, show a small chart of requests stripped or rejected at the edge, a chart of the 50th and 99th percentile of baggage size against the limit, and a table of unknown keys seen in the last day. Add one chart for the number of distinct values in each copied telemetry attribute. When someone asks how safe your baggage use is, this page answers in half a minute.
Alerts worth having
- Any baggage header seen on traffic leaving to outside hosts.
- A sudden rise in stripped or rejected baggage at the edge.
- The 99th percentile of baggage size creeping toward the limit.
- An unknown key appearing in more than a handful of requests.
- A copied attribute whose number of distinct values grows faster than expected.
When something goes wrong
Contain
Tighten the edge rule to strip everything, switch off copying to telemetry, and block outbound baggage at the egress proxy. These are quick switches prepared in advance.
Find the scope
Use the registry and monitoring to learn which keys, which services, which stores, and which time period were involved.
Clean up
Remove or mask the affected data in logs, traces, and queues. If an outside party received data, contact them under your agreement and ask for deletion.
Tell the right people
If personal data was involved, your privacy and legal teams decide what notices are required, and when.
Learn and fix the cause
Add a test that would have caught the problem, and a rule that makes the unsafe way impossible, not just discouraged.
21Deployment and Cloud
Principles become real when mapped to the building blocks you actually run. Here is a map for a typical AWS-style environment. Always check current product features before relying on them.
| Need | Typical building block | Note |
|---|---|---|
| Strip or rewrite inbound headers | A reverse proxy or service mesh ingress; API gateway request mapping; edge functions on a content delivery network | Choose the layer that all traffic passes through. A plain load balancer may not offer arbitrary header removal |
| Limit header size and flood abusive clients | A web application firewall with size and rate rules | Add as a coarse first filter. Fine-grained validation stays in the gateway |
| Control outbound calls | Egress proxy in a controlled subnet, plus domain allow lists in a network firewall | Make the proxy the only route out, so there is no bypass |
| Know the caller’s identity | Mutual TLS through a mesh, or signed service credentials | Gives baggage entries a known sender, not proof of truth |
| Protect messages at rest | Encryption with managed keys, short retention, restricted read permissions on queues and dead-letter queues | Message systems also cap attributes, so baggage must compete for room |
| Control who reads logs and traces | Fine-grained permissions, short retention, separate stores for sensitive logs | Review roles that have broad read access |
| Telemetry safety net | Collector rules to remove unapproved attributes and mask patterns | Test them as carefully as application code |
| Configuration as code | Propagator settings, limits, and allow lists in version control, rolled out gradually | Registry changes generate configuration for all layers |
Roll out in a safe order
Measure first
Turn on counting at the edge and at the exit, without changing any behavior. Learn what baggage exists today.
Strip untrusted inbound baggage
This is the highest benefit and the lowest risk, because almost no legitimate outside client depends on it.
Block outbound leaks
Start with the egress proxy removing baggage for outside hosts. Watch for partners that really depended on it.
Adopt the registry and helper
Migrate services one by one, with the lint rule catching direct use.
Clean up telemetry
Replace “copy all” with allow-listed copies, and remove old attributes with sensitive values from storage according to policy.
22Advantages, Disadvantages, Trade-offs, and Alternatives
After all these risks, a fair question arises: is baggage worth using at all?
What baggage gives you
- Context available everywhere, without extra lookups.
- No need to change every function and message format to pass a hint along.
- Consistent behavior across languages and frameworks.
- A way to label telemetry with business meaning.
- Very low cost for small, well-chosen entries.
What it costs in risk
- Automatic spread to places you did not choose.
- No proof that values are honest or unchanged.
- Plain-text exposure in logs, queues, and partner systems.
- Amplification of size across internal calls.
- Easy to over-use, since adding an entry takes one line.
The alternatives, side by side
| Approach | How it works | Trust | Cost and effort | Best for |
|---|---|---|---|---|
| Baggage with controls | Small hints in a header, with edge, helper, and egress guards | Low. Hints only | Low once the controls exist | Experiment cohorts, region hints, debug labels |
| Signed token | A tamper-evident token that each service verifies | High | Medium. Key management and token size | Identity, tenant, and permissions |
| Lookup by identifier | Pass a short opaque ID, and each service asks a trusted store for facts | High, as high as the store | Medium. Extra calls and caching | Customer attributes, plan details |
| Explicit parameters | Put the value in the request or message body as a normal field | As high as normal input validation | Medium. Interface changes | Anything a partner or a single next service needs |
| Request context store | Save request facts in a fast shared cache keyed by trace or request ID | High, with access control | Higher. A new component to run | Larger or more sensitive context |
How to decide in a meeting
When a team proposes a new baggage entry, run through a quick decision. If the value is secret or personal, the answer is no. If a decision depends on it, use a token or a lookup. If it must reach a partner, use an explicit field. If it is a small hint with a fixed set of values and has an owner, accept it and add it to the registry. This takes five minutes, and it stops the slow growth of risky entries that are only visible years later.
Trade-offs to discuss with your team
- Convenience versus control. Open, automatic baggage is the easiest to use and the hardest to control. A registry and helper add friction and add safety.
- Reach versus exposure. The wider the entries travel, the more services benefit and the more places can leak them.
- Hints versus certainty. Baggage is cheap because it is unverified. Verification costs effort, and belongs in other tools.
- One central policy versus team freedom. A central registry slows new keys down a little and prevents the slow growth of unreviewed ones.
23Design Patterns and Anti-patterns
Habits that work for many teams, and the traps that catch many teams.
Patterns that work
Strip at the door
Remove untrusted inbound baggage at the edge, and set fresh trusted values from verified facts.
Allow lists everywhere
Named keys only, at the edge, in the helper, in telemetry copies, in queues, and at the exit.
Hints, not decisions
No permission, price, or tenant choice depends on a baggage value.
One shared helper
Every read and write passes through one validated, counted, budgeted library.
Origin prefix on copies
Telemetry attributes copied from baggage carry a visible prefix, reminding readers that the value is a hint.
Default-deny egress
No baggage leaves toward outside parties unless a written exception exists.
Registry as code
One list of keys generates all allow lists and tests, with owners and review dates.
Marker-based leak tests
A unique canary value is followed through every store and every outbound path, on a schedule.
Anti-patterns to avoid
What people do
Put the user’s email or name into baggage “so that support can see it in every log”.
Why it hurts
The value is sent to every service and every outside partner, and is stored in every place that copies it. A convenience for one team becomes a data-protection problem for the whole company.
Do this instead
Carry a short, opaque request or customer reference, and let authorized support tools look up details from a protected store.
What people do
Use a baggage entry to choose the tenant, the user role, or a price.
Why it hurts
Anyone who can send a header, or who can change it somewhere along the path, can claim any tenant or role. This is a direct path to data exposure or free goods.
Do this instead
Derive tenant and role from the verified identity at each service, using a signed token or a trusted lookup.
What people do
Switch on “copy all baggage to every span” and every log line.
Why it hurts
Outsiders can write free text into your telemetry, cardinality can explode, and sensitive values are stored for as long as the telemetry lives.
Do this instead
Copy a short allow-listed set of keys, with an origin prefix and value limits.
What people do
Leave default propagation on for every outgoing call, including outside partners, and never check.
Why it hurts
Entries leak to outside parties by default, silently, forever.
Do this instead
Put an egress proxy or filtering propagator in place, and count what still leaves.
24Best Practices and Common Mistakes
A short list of habits to adopt, and the slips that appear in review after review.
Best practices
- Treat baggage as untrusted input at every boundary. It is a postcard written by strangers.
- Keep it tiny. A few short, coarse entries with fixed sets of values.
- Never carry secrets or personal data. Use opaque references and trusted lookups.
- Strip at the edge and filter at the exit. The two boundaries matter most.
- Make decisions from verified facts and use baggage only for hints.
- Maintain a registry with owners, purposes, and review dates.
- Copy to telemetry with an allow list and never copy to metrics unless the values form a small fixed set.
- Test and monitor continuously. Marker tests, malformed input tests, size distributions, and egress counters.
Common mistakes and quick fixes
| Mistake | What you will notice | Fix |
|---|---|---|
| Trusting incoming baggage from the internet | Strange values from clients, forged flags in logs | Strip or allow-list at the edge, set trusted values there |
| Sending baggage to partners by accident | Partner support asks why they see your internal labels | Egress proxy and filtering propagator, plus counters |
| No size limits | Requests failing with header errors, rising internal traffic | Set small limits at the edge and in the helper |
| Copying baggage to metric labels | Series counts and bills jump | Remove those labels, allow only small fixed sets |
| Pasting values into logs, queries, or pages | Odd log lines, security scan findings | Validate and encode, use parameterized queries |
| Leaving scopes open in thread pools | Wrong tenant labels under load | Close scopes safely, use context-aware executors, start clean |
| Putting baggage on messages by default | Old values seen long after, operators see sensitive entries | Allow only named keys on named topics, shorten retention |
| No owner for keys | Nobody knows what a key is for, so nobody dares remove it | Registry with owners and review dates |
| Allowing the baggage header from every website in a browser app | Users or third-party pages can inject entries | Restrict allowed origins, avoid sending baggage from browsers |
The single most effective week of work is usually the first one: measure what baggage exists, strip it at the edge, and block it at the exit. Everything after that is refinement.
25Real-World and Industry Examples
The guidance in this guide matches what standards bodies, tool makers, and infrastructure defaults already tell us, plus two clearly labeled made-up stories.
The OpenTelemetry documentation: a direct warning
OpenTelemetry’s own explanation of baggage includes a security note. It points out that because baggage is propagated automatically, sensitive items can reach unintended parties such as outside APIs, and that there are no built-in integrity checks, so values you read may not be yours. The approach in this guide turns those two warnings into concrete controls at the edge, in the services, and at the exit. Read the current documentation yourself, as wording and guidance may change.
The W3C Baggage specification: limits and privacy notes
The specification that defines the baggage header sets minimum limits for the number and size of entries, and discusses privacy and security concerns, including the risk of sensitive data traveling beyond its intended audience. The limits are floors that implementations must support, not recommendations, which is why this guide suggests setting your own much smaller limits.
OpenTracing and Jaeger: baggage with restrictions
OpenTracing, one of the projects that merged into OpenTelemetry, already had baggage and described its cost, since every hop carries every entry. Jaeger’s client libraries offered a way to restrict baggage to approved keys and to cap value sizes, configured centrally. That is the same idea as the registry and allow lists described here, which shows that the problem is old and the remedy is well understood.
Everyday infrastructure: header size limits
Common web servers and runtimes cap header sizes by default, typically around eight thousand bytes for a single header line in popular servers, with some runtimes allowing more. A baggage header that grows close to such a cap can cause ordinary requests to fail. Because defaults differ between products and versions, check your own stack, and choose baggage limits that sit comfortably below the smallest cap on the path.
Proxies and gateways: header removal as a standard feature
Widely used proxies and gateways, such as Envoy and NGINX, can remove, replace, or add request headers by rule. Many API gateway products offer similar mapping features. This is why edge stripping and egress filtering are practical today without writing new software.
Harbor Foods (made up for teaching)
Harbor Foods, the imaginary company from Chapter 5, ran a leak test with a marker value after the shipping partner incident. The marker showed up in the partner’s request logs, in a queue read by a data-science team, and in a debugging dashboard. The team stripped baggage at the edge, added an egress proxy, replaced the email entry with an opaque reference, and added a registry. A second marker test, a month later, found the value only in the places the registry allowed. Treat the story as a picture of the method, not as a promise of results.
A cardinality scare (made up for teaching)
Imagine a video service in which a developer enabled “copy all baggage to metric labels” while testing. A traffic scanner then sent random values in the baggage header at a steady pace. Within hours, the number of metric series grew until the backend slowed and dashboards timed out. The fix was to remove the copy rule, strip untrusted baggage at the edge, and allow only small fixed-set labels. The lesson is that an outsider can use your own settings against you.
26Frequently Asked Questions
Short answers to the questions that come up most often.
27Summary and Key Takeaways
Key Takeaways
- Baggage is a postcard. It is plain text, unverified, and automatically sent to every hop, so carry only small, coarse, harmless hints.
- Guard both boundaries. Strip or allow-list inbound baggage at the edge, and block or filter it on the way to outside parties.
- Hints, never decisions. Permissions, tenants, and prices must come from verified identity and trusted lookups.
- Limit size and shape. Small caps on entries and bytes stop amplification, and fixed sets of values stop injection and cardinality explosions.
- Mind the copies. Telemetry, logs, and queues store baggage for a long time, so copy only allow-listed keys, label their origin, and keep metrics free of free-form values.
- Keep the room clean. Close scopes, use context-aware executors, and start each request and job with a fresh context so values never bleed between requests or tenants.
- Govern, test, and watch. A registry with owners, marker-based leak tests, and counters at the edge and exit keep the whole design honest over time.