> For the complete documentation index, see [llms.txt](https://docs.groundcover.com/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://docs.groundcover.com/collect-data/data-pipelines/log-pipelines/sensitive-attributes.md).

# Sensitive Attributes

#### Overview

Sensitive attributes let you mask a log attribute for most users while keeping the real value available to the people who need it. When a log is processed, groundcover replaces the value with a placeholder (`*****` by default). The original value goes to a separate, access-controlled column. Users whose RBAC policy grants **sensitive data access** can reveal the real value. Everyone else sees only the masked one.

This differs from [Obfuscate Logs](/collect-data/data-pipelines/log-pipelines/obfuscate-logs.md). Obfuscation removes the original value for good. A sensitive attribute keeps the original value in storage and controls who can read it.

**Best for:** Values that most users must not see but that some teams still need, such as customer IDs, account numbers, or email addresses needed during an investigation.

{% hint style="info" %}
Sensitive attributes are supported for **logs** only. They require groundcover version **1.13.12** and above.
{% endhint %}

#### How It Works

Setting up sensitive attributes takes four steps:

1. **Enable the feature on the backend.**
2. **Grant access in a policy.** Turn on **Allow access to sensitive data** in the RBAC policies of the users who should see real values.
3. **Mark attributes as sensitive.** Add an OTTL rule that calls `sensitive()` on each attribute you want to protect.
4. **Query and reveal.** Users see the masked value by default. Users with access can reveal it in the log drawer or query it with `sensitive.<key>`.

Let's go over each of these steps:

***

#### 1. Configure the Backend

Turn the feature on in the groundcover backend Helm values:

```yaml
global:
  clickhouse:
    sensitiveRole:
      enabled: true
```

{% hint style="info" %}
Applying the sensitiveRole configuration in the backend will result in a restart to Clickhouse. During the restart, Clickhouse queries will result in failure message. There won't be any data loss as a result of the restart.
{% endhint %}

#### 2. Grant Access in RBAC Policies

No policy grants sensitive access by default. This includes the default Admin, Editor, and Viewer policies. You must turn it on explicitly in each policy that needs it.

**From the UI**

1. Go to **Settings → Policies** and create or edit a custom policy.
2. Set the **Data Scope** to **Simple** or **Advanced**.
3. Turn on **Allow access to sensitive data**.

The toggle appears only after the backend reports the `sensitive-data` feature (see step 1). It is hidden when the data scope is set to **No data**.

**Through the API**

Set `sensitiveAccess` to `true` when you create or update a policy:

```json
{
  "name": "Payments on-call",
  "role": { "read": "read" },
  "dataScope": { ... },
  "sensitiveAccess": true
}
```

{% hint style="warning" %}
`sensitiveAccess` is not a partial update. A policy update request that leaves out `sensitiveAccess` sets it to `false`. Always include it when you update a policy that should keep sensitive access.
{% endhint %}

**How the data scope limits reveal**

Sensitive access follows the policy's **logs data scope**. A user can reveal values only on log rows that match the scope of a policy granting sensitive access:

* A policy with **full data scope** and sensitive access lets the user reveal values on every log.
* A policy scoped to `namespace = payments` with sensitive access lets the user reveal values only on logs from the `payments` namespace. On other logs, the user still sees the masked value.

**Multiple policies**

When a user has more than one policy:

* Only policies with **Allow access to sensitive data** turned on count toward sensitive access. A policy without the toggle never widens it, even if that policy has a broader data scope.
* The scopes of all granting policies merge with **OR** logic, just like regular [data scope merging](/administer/role-based-access-control-rbac.md#multiple-policies).

**Example:** A user has Policy A (Viewer, all clusters, no sensitive access) and Policy B (Viewer, `cluster = prod-eu`, sensitive access). They can see logs from every cluster, but can reveal sensitive values only on `prod-eu` logs.

***

#### 3. Mark Attributes as Sensitive with OTTL

Use the `sensitive` function in a [logs pipeline](/collect-data/data-pipelines/log-pipelines.md) rule to mark an attribute as sensitive.

{% hint style="info" %}
Since Logs Pipeline rules are being applied upon ingestion of logs, only new logs will be impacted. Existing logs will have unmasked values kept.
{% endhint %}

**Usage**

```yaml
sensitive(attributes["<key>"])
sensitive(attributes["<key>"], "<replacement>")
```

**Arguments:**

| Argument      | Required | Description                                                          |
| ------------- | -------- | -------------------------------------------------------------------- |
| `target`      | Yes      | A single attribute, `attributes["<key>"]`.                           |
| `replacement` | No       | The masked value shown in place of the original. Defaults to `*****` |

**What it does to each matching log:**

* The attribute keeps its key, but its value is replaced with `replacement`. This is the value most users see.
* The original value is saved under the same key in an access-controlled column.
* If the attribute is missing or empty, nothing happens.

**Basic Configuration**

Mark a single attribute as sensitive:

```yaml
ottlRules:
  - ruleName: "sensitive_customer_id"
    statements:
      - 'sensitive(attributes["customer_id"])'
```

💡 **Example:** `customer_id: "cus_9f3a1b"` is stored as `customer_id: "*****"`. Users with sensitive access can reveal `cus_9f3a1b`.

**Multiple Attributes and a Custom Replacement**

A rule can mark several attributes. Each statement handles one attribute:

```yaml
ottlRules:
  - ruleName: "sensitive_payment_fields"
    conditions:
      - 'workload == "payment-service"'
    statements:
      - 'sensitive(attributes["account_number"], "[MASKED]")'
      - 'sensitive(attributes["card_holder"], "[MASKED]")'
      - 'sensitive(attributes["email"])'
```

**Parse First, Then Mark as Sensitive**

`sensitive` works only on attributes, so values inside the log body must be extracted first. `sensitive` sees changes made by earlier rules in the same pipeline, so you can parse in one rule and mark the result in a later one:

```yaml
ottlRules:
  - ruleName: "parse_json"
    conditions:
      - 'workload == "auth-service"'
    statements:
      - 'merge_maps(attributes, ParseJSON(body), "insert")'
  - ruleName: "sensitive_user_email"
    conditions:
      - 'workload == "auth-service"'
    statements:
      - 'sensitive(attributes["user_email"])'
```

{% hint style="warning" %}
`sensitive` masks only the attribute. If the same value also appears in the log **body**, it stays visible there. To hide it from the body as well, combine `sensitive` with [`obfuscate_pii` or `replace_pattern`](/collect-data/data-pipelines/log-pipelines/obfuscate-logs.md) on `body`.
{% endhint %}

{% hint style="info" %}
Unlike the other obfuscation functions, `sensitive` does not discard the original value. It is sent to your groundcover backend and stored there, readable only through groundcover. If a value must never leave your cluster, use [Obfuscate Logs](/collect-data/data-pipelines/log-pipelines/obfuscate-logs.md) instead.
{% endhint %}

Use the [Parsing Playground](/collect-data/data-pipelines/log-pipelines.md#parsing-playground) to test your rule against a real log before saving it.

***

#### 4. Query and Use Sensitive Attributes

**What users see by default**

Logs queries return the **masked** value for a sensitive attribute, whatever the user's permissions. Searching, grouping, and showing `customer_id` all use the masked value, e.g. `*****` by default. A user must ask for the real value explicitly.

**Revealing a value in the log drawer**

Open a log and go to its **Attributes** tab:

* **Users with sensitive access** for that log see an **eye** icon next to each sensitive attribute. Clicking it fetches the real value for that one log only. Clicking again masks it.
* **Users without access** see the masked value with a **lock** icon. Hovering over the icon explains that they don't have permission to view the value.

Revealed values aren't kept on the page. Closing the drawer masks them again.

**Querying real values with `sensitive.<key>`**

Add the `sensitive.` prefix to an attribute key to read its real value in a logs query:

| Usage          | Example                                                    |
| -------------- | ---------------------------------------------------------- |
| Filter         | `sensitive.customer_id:"cus_9f3a1b"`                       |
| Select a field | `workload:payment-service \| fields sensitive.customer_id` |
| Rename a field | `\| fields sensitive.customer_id as customer`              |

How `sensitive.<key>` resolves depends on the row and the user:

* The user has sensitive access and the log row is within their sensitive scope: the **real** value.
* The user has no sensitive access, or the row is outside their scope: the **masked** value, the same as querying `<key>` directly. No error is returned.
* The row has no sensitive value for that key: the regular attribute value.

The logs search bar shows a **Sensitive Data Access** hint (`sensitive.key:value`) when the selected backend supports the feature.

**Where real values are never shown**

* **Grafana.** The built-in Grafana datasource uses a default user, which never has the relevant permissions. It always returns masked values.
* **`SELECT *` over the logs tables.** The column holding the sensitive attributes is excluded, so queries that use `SELECT *` never include it.

***

#### Troubleshooting

| Symptom                                                                    | Likely cause                                                                                                                                                                                 |
| -------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| The **Allow access to sensitive data** toggle doesn't appear               | The backend doesn't report `sensitive-data` yet. Check that `sensitiveRole.enabled` is `true`, the ClickHouse rolling restart has finished, and db-manager has finished setup on every shard |
| `sensitive.<key>` returns the masked value                                 | The user has no policy with sensitive access, or the log is outside the scope of their granting policies                                                                                     |
| The attribute is still shown in plain text                                 | The `sensitive` rule didn't match. Check the rule's `conditions` and that the attribute exists when the rule runs. Use the Parsing Playground to test                                        |
| The value is masked in attributes but visible in the log body              | `sensitive` masks attributes only. Add `obfuscate_pii` or `replace_pattern` on `body`                                                                                                        |
| The rule fails to save with `sensitive target must be attributes["<key>"]` | The target is `body`, a whole map, or a column. Target a single attribute key                                                                                                                |


---

# Agent Instructions
This documentation is published with GitBook. GitBook is the documentation platform designed so that both humans and AI agents can read, navigate, and reason over technical content effectively. Learn more at gitbook.com.

## Querying This Documentation
If you need additional information that is not directly available in this page, you can query the documentation dynamically by asking a question.

Perform an HTTP GET request on the following URL with the `ask` and `goal` query parameters:

```
GET https://docs.groundcover.com/collect-data/data-pipelines/log-pipelines/sensitive-attributes.md?ask=<question>&goal=<user_goal>
```

`ask` is the immediate question: it should be specific, self-contained, and written in natural language.
`goal` is what the user is ultimately trying to achieve, the reason they need the answer. Sharing it helps GitBook give you a better, more relevant answer. A goal is most helpful when it describes the outcome the user wants rather than restating the question. For example, with `ask=how do I create an API token`, a goal like `build a script that syncs our docs to a CMS` lets GitBook tailor the answer to that use case.

The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
