TT Lab
Get started
Learn Learning paths Courses

OTCA — OpenTelemetry Certified Associate

Well-propagated baggage is not proof of permission

Continue in TT Lab

In one line

Baggage propagation is a feature that carries information. In this lab, you read an external value without mixing it with the local request, and then send out only the specified keys and values to the specified destination. You do not turn the fact that something is readable into a guarantee of authentication, authorization, or personal data safety.

Why this was needed

Suppose you attached region and channel to an order request headed for the inventory service. The operator wants to split out slow segments by region and entry path. But someone also put role=admin or an email-shaped value into the same baggage. Code that injects the whole thing into another server is syntactically correct, but it also carries information that is not needed across the boundary.

A more dangerous misunderstanding is to judge it an administrator request because role=admin is visible. The inputs of this module are synthetic headers that anyone can make. A function that reads a value and a system that verifies a user's permissions are entirely separate, and the lab has no authentication server at all. From the observation that a header passed through, you cannot claim that permission verification succeeded.

The OpenTelemetry documentation explains that baggage has no built-in integrity checking and that you must be careful about exposing the information it carries. Also, baggage does not automatically go into span attributes. Official Baggage explanation. This module confirms that warning with counterexamples in small input and output functions.

How it works

The incoming Context and the Context that was already attached

In the preliminary experiment, we attached request=alpha to the caller and then extracted external baggage. The default extraction result contained not only the external values but also the existing local request. In the extraction that explicitly specified an empty OpenTelemetry Context, only the external values remained. We first expected the two results to be the same, but the actual run proved us wrong, and we made that difference the assignment.

Extraction condition in this lab Observed baggage
Default extraction with a local request present The local request and the external values are together
Extraction with an empty Context specified Only the external values
Empty Context and an empty header Empty baggage

The inbound of step 6 is a function that reads the external header in a separate context. It must return a new OTel Context without changing the input dict or the caller's current state. The external role=admin also remains in the result it reads at this step. This does not mean to trust or use it; it is a condition for observing "extraction" and "selection by policy" as different operations.

The empty Context here is OpenTelemetry's Context. It is similar in name to the contextvars.Context used earlier with the executor, but it is not an object with the same return contract. Check what each function was told to return. If the type is wrong, even if the values happen to be the same, it will not connect properly to the next propagation API.

Reading from an empty context can prevent mixing with the existing request. But that does not make it possible to know whether the external value itself is genuine. You must state this limit clearly so that another cure-all rule like "a new Context makes it safe" does not arise. Business permissions must be judged from a separately verified identity and policy.

Is it enough to allow only keys?

Step 7 fixes the outbound(source, destination) function. Allowing only the name region does not mean any string placed inside it may be sent. If someone put a long description or an identifier in the region value, then even though the name is on the allowlist, the content falls outside the assignment's classification scheme. That is why this assignment also closes the set of values.

Condition What this assignment allows
Destination string Exactly equal to warehouse.internal
region value test-east or test-west
channel value web or batch
Any other key, value, or destination Not propagated

The names and values are synthetic classifications for the lab, not real operational information. Because the allowed values are a finite set of short strings, strings of arbitrary length are also excluded within the assignment. But we do not call this a general personal data policy that suits every service. A real policy must be set together with the purpose of the information, the recipients, the retention period, and the accessing parties, and this lab does not test that whole system. What is verified here is whether the function followed the table above exactly.

It does not fix case or whitespace arbitrarily either. WEB, or test-east followed by a space, is not an allowed value under this contract. Whether to normalize or reject the input is a choice to be made in interface design. This assignment specified that only allowed strings are passed on, rather than silent normalization.

The destination boundary is not a string prefix

warehouse.internal.evil starts with warehouse.internal but is not the same destination. The lab feeds this counterexample into the real function. If you allow it with startswith alone, you fail the destination policy even if the region value filter is right. For any other destination you must return an empty dict, and even for an allowed destination, if no value remains, you do not create even an empty baggage header.

There is an important scope limit. Here destination is a logical destination identifier. DNS lookups, URL parsing, TLS certificates, and HTTP redirects are not executed. So you must not say that passing this function's string comparison means you have completed an actual HTTP client's destination validation or SSRF defense. For a system with a network connection, that boundary must be designed and tested separately.

Build the context to send instead of deleting the original

The student function picks only the needed values from source, puts them into a new Context, and builds a header with the W3C propagator. The task is not to change the original itself just because the original's role or other values are unnecessary. The original may also be used in the processing of other policies, so this function's contract is preservation. The grader re-extracts the propagated result to compare, and also checks the original before and after.

The order of the header string does not have to be exactly the same as the correct answer. If it is W3C baggage with the same meaning, it will not fail you because of key order. Instead, it fails if a key that is not allowed leaks out under a different header name. The returned dict may be only one baggage string header or an empty dict. This is so that the grading does not become strict about order and loose about leakage.

Baggage and span attributes are different observations

In the preliminary experiment, we ended a real SDK span with baggage attached and read it at the exporter. The baggage did not appear in the span attributes automatically. The corresponding hypothesis in the step 8 report is answered on the basis of this observation and the official explanation. Steps 1–7 of the current lab verify the behavior of Context functions, and they do not create spans or store to a Collector at every step.

Separating the information to propagate from the information to record on the span also makes the investigation plan clearer. Which keys went out in the header, whether they entered the receiving Context, whether they were selectively recorded in span attributes, and whether they remained in storage are each a different question. Do not report as if you had checked the other boundaries because you saw a value in one place.

What it looks like in the field

If you do a code review based on this exercise, you do not look at just one normal header. You check together an empty header, an unknown key, a wrong value for a known key, a different destination, a prefix that looks like an allowed host, and the case where the original request has local values. Even without real data, you can build a counterexample for each boundary. If you first reproduce the failure conditions with synthetic data, you can narrow down the code's responsibility without collecting unnecessary operational information.

What you will do in the next lab

Step 6 reads an incoming value separately from the existing request. Step 7 builds the header to send out using the destination, key, and value policy in the table. Step 8 judges eight hypotheses about propagation timing, restoration, and trust scope. This is not an assignment that passes with only the report being right. The overall check passes only when the actual behavior of the earlier seven functions is all correct, and the scope of what passes is not widened to external authentication or network security as a whole.