role: Aggregator
replicas: ${replicas}
podAnnotations:
  reloader.stakater.com/auto: "true"
service:
  enabled: true
  type: ClusterIP
  ports:
    - name: otlp-grpc
      port: 4317
      protocol: TCP
    - name: otlp-http
      port: 4318
      protocol: TCP
env:
  - name: DATADOG_API_KEY
    valueFrom:
      secretKeyRef:
        name: vector-datadog
        key: api_key
resources:
  requests:
    cpu: ${requests_cpu}
    memory: ${requests_memory}
  limits:
    cpu: "${limits_cpu}"
    memory: ${limits_memory}
podDisruptionBudget:
  enabled: true
  minAvailable: 2

topologySpreadConstraints:
  - maxSkew: 1
    topologyKey: topology.kubernetes.io/zone
    whenUnsatisfiable: ScheduleAnyway
    labelSelector:
      matchLabels:
        app.kubernetes.io/name: vector

affinity:
  podAntiAffinity:
    preferredDuringSchedulingIgnoredDuringExecution:
      - weight: 100
        podAffinityTerm:
          topologyKey: kubernetes.io/hostname
          labelSelector:
            matchLabels:
              app.kubernetes.io/name: vector

%{ if service_monitor_enabled ~}
serviceMonitor:
  enabled: true
%{ endif ~}
customConfig:
  data_dir: /vector-data-dir
  api:
    enabled: true
    address: 127.0.0.1:8686
  sources:
    otlp_in:
      type: opentelemetry
      grpc:
        address: 0.0.0.0:4317
      http:
        address: 0.0.0.0:4318
  transforms:
    drop_audit:
      type: filter
      inputs:
        - otlp_in.logs
      condition: |
        msg = if is_string(.message) { string!(.message) } else { "" }
        ${audit_drop_condition}
%{ if email_redaction_enabled ~}
    redact_emails_logs:
      type: remap
      inputs:
        - drop_audit
      source: |
        email_re = r'[\w._%+\-]+@[\w.-]+\.[A-Za-z]{2,}'
        if is_string(.message) {
          .message = replace(string!(.message), email_re, "${email_redaction_replacement}")
        }
        if is_object(.attributes) {
          attrs, _err = object(.attributes)
          if is_object(attrs) {
            .attributes = map_values(attrs) -> |v| {
              if is_string(v) { replace(string!(v), email_re, "${email_redaction_replacement}") } else { v }
            }
          }
        }
    redact_emails_traces:
      type: remap
      inputs:
        - otlp_in.traces
      source: |
        email_re = r'[\w._%+\-]+@[\w.-]+\.[A-Za-z]{2,}'
        if is_object(.attributes) {
          attrs, _err = object(.attributes)
          if is_object(attrs) {
            .attributes = map_values(attrs) -> |v| {
              if is_string(v) { replace(string!(v), email_re, "${email_redaction_replacement}") } else { v }
            }
          }
        }
        if is_object(.resource) {
          res, _err = object(.resource)
          if is_object(res) && is_object(res.attributes) {
            rattrs, _err2 = object(res.attributes)
            if is_object(rattrs) {
              .resource.attributes = map_values(object!(rattrs)) -> |v| {
                if is_string(v) { replace(string!(v), email_re, "${email_redaction_replacement}") } else { v }
              }
            }
          }
        }
    redact_emails_metrics:
      type: remap
      inputs:
        - otlp_in.metrics
      source: |
        email_re = r'[\w._%+\-]+@[\w.-]+\.[A-Za-z]{2,}'
        if is_object(.attributes) {
          attrs, _err = object(.attributes)
          if is_object(attrs) {
            .attributes = map_values(attrs) -> |v| {
              if is_string(v) { replace(string!(v), email_re, "${email_redaction_replacement}") } else { v }
            }
          }
        }
        if is_object(.resource) {
          res, _err = object(.resource)
          if is_object(res) && is_object(res.attributes) {
            rattrs, _err2 = object(res.attributes)
            if is_object(rattrs) {
              .resource.attributes = map_values(object!(rattrs)) -> |v| {
                if is_string(v) { replace(string!(v), email_re, "${email_redaction_replacement}") } else { v }
              }
            }
          }
        }
%{ endif ~}
  sinks:
    datadog_logs:
      type: datadog_logs
      inputs:
        - ${email_redaction_enabled ? "redact_emails_logs" : "drop_audit"}
      default_api_key: $${DATADOG_API_KEY}
      site: ${datadog_site}
    datadog_traces:
      type: datadog_traces
      inputs:
        - ${email_redaction_enabled ? "redact_emails_traces" : "otlp_in.traces"}
      default_api_key: $${DATADOG_API_KEY}
      site: ${datadog_site}
    datadog_metrics:
      type: datadog_metrics
      inputs:
        - ${email_redaction_enabled ? "redact_emails_metrics" : "otlp_in.metrics"}
      default_api_key: $${DATADOG_API_KEY}
      site: ${datadog_site}

-              .resource.attributes = map_values(object!(rattrs)) -> |v| {

+              .resource.attributes = map_values(rattrs) -> |v| {


Now fixing the same warning in the standalone VRL test programs (so the test runners don't regress) and re-running them.


Bash

grep -rn "object!(attrs)\|object!(rattrs)" /Users/steve.briggs/projects/IAC/terraform/internal-modules/kubernetes/monitoring/vector/tests/ 2>&1

Find object! wrappers in VRL test programs


/Users/steve.briggs/projects/IAC/terraform/internal-modules/kubernetes/monitoring/vector/tests/vrl/redact_emails_metrics/program.vrl:6:    .attributes = map_values(object!(attrs)) -> |v| {
/Users/steve.briggs/projects/IAC/terraform/internal-modules/kubernetes/monitoring/vector/tests/vrl/redact_emails_metrics/program.vrl:17:      .resource.attributes = map_values(object!(rattrs)) -> |v| {
/Users/steve.briggs/projects/IAC/terraform/internal-modules/kubernetes/monitoring/vector/tests/vrl/redact_emails_logs/program.vrl:10:    .attributes = map_values(object!(attrs)) -> |v| {
/Users/steve.briggs/projects/IAC/terraform/internal-modules/kubernetes/monitoring/vector/tests/vrl/redact_emails_traces/program.vrl:6:    .attributes = map_values(object!(attrs)) -> |v| {
/Users/steve.briggs/projects/IAC/terraform/internal-modules/kubernetes/monitoring/vector/tests/vrl/redact_emails_traces/program.vrl:17:      .resource.attributes = map_values(object!(rattrs)) -> |v| {


1	email_re = r'[\w._%+\-]+@[\w.-]+\.[A-Za-z]{2,}'
2
3	if is_string(.message) {
4	  .message = replace(string!(.message), email_re, "[REDACTED-EMAIL]")
5	}
6
7	if is_object(.attributes) {
8	  attrs, _err = object(.attributes)
9	  if is_object(attrs) {
10	    .attributes = map_values(object!(attrs)) -> |v| {
11	      if is_string(v) { replace(string!(v), email_re, "[REDACTED-EMAIL]") } else { v }
12	    }
13	  }
14	}
15	.
16

email_re = r'[\w._%+\-]+@[\w.-]+\.[A-Za-z]{2,}'

if is_string(.message) {
  .message = replace(string!(.message), email_re, "[REDACTED-EMAIL]")
}

if is_object(.attributes) {
  attrs, _err = object(.attributes)
  if is_object(attrs) {
    .attributes = map_values(object!(attrs)) -> |v| {
      if is_string(v) { replace(string!(v), email_re, "[REDACTED-EMAIL]") } else { v }
    }
  }
}
.


1	email_re = r'[\w._%+\-]+@[\w.-]+\.[A-Za-z]{2,}'
2
3	if is_object(.attributes) {
4	  attrs, _err = object(.attributes)
5	  if is_object(attrs) {
6	    .attributes = map_values(object!(attrs)) -> |v| {
7	      if is_string(v) { replace(string!(v), email_re, "[REDACTED-EMAIL]") } else { v }
8	    }
9	  }
10	}
11
12	if is_object(.resource) {
13	  res, _err = object(.resource)
14	  if is_object(res) && is_object(res.attributes) {
15	    rattrs, _err2 = object(res.attributes)
16	    if is_object(rattrs) {
17	      .resource.attributes = map_values(object!(rattrs)) -> |v| {
18	        if is_string(v) { replace(string!(v), email_re, "[REDACTED-EMAIL]") } else { v }
19	      }
20	    }
21	  }
22	}
23
24	.
25

email_re = r'[\w._%+\-]+@[\w.-]+\.[A-Za-z]{2,}'

if is_object(.attributes) {
  attrs, _err = object(.attributes)
  if is_object(attrs) {
    .attributes = map_values(object!(attrs)) -> |v| {
      if is_string(v) { replace(string!(v), email_re, "[REDACTED-EMAIL]") } else { v }
    }
  }
}

if is_object(.resource) {
  res, _err = object(.resource)
  if is_object(res) && is_object(res.attributes) {
    rattrs, _err2 = object(res.attributes)
    if is_object(rattrs) {
      .resource.attributes = map_values(object!(rattrs)) -> |v| {
        if is_string(v) { replace(string!(v), email_re, "[REDACTED-EMAIL]") } else { v }
      }
    }
  }
}

.


i get this error when deploying vector; WARN transform{component_kind="transform" component_id=redact_emails_logs component_type=remap}: vector::transforms::remap: VRL compilation warning. warnings= w…


Implement Vector with Flux for data redaction


The file /Users/steve.briggs/projects/IAC/terraform/internal-modules/kubernetes/monitoring/vector/tests/vrl/redact_emails_logs/program.vrl has been updated. All occurrences were successfully replaced. (file state is current in your context — no need to Read it back)

email_re = r'[\w._%+\-]+@[\w.-]+\.[A-Za-z]{2,}'

if is_string(.message) {
  .message = replace(string!(.message), email_re, "[REDACTED-EMAIL]")
}

if is_object(.attributes) {
  attrs, _err = object(.attributes)
  if is_object(attrs) {
    .attributes = map_values(object!(attrs)) -> |v| {
      if is_string(v) { replace(string!(v), email_re, "[REDACTED-EMAIL]") } else { v }
    }
  }
}
.

-    .attributes = map_values(object!(attrs)) -> |v| {

+    .attributes = map_values(attrs) -> |v| {


The file /Users/steve.briggs/projects/IAC/terraform/internal-modules/kubernetes/monitoring/vector/tests/vrl/redact_emails_traces/program.vrl has been updated. All occurrences were successfully replaced. (file state is current in your context — no need to Read it back)

email_re = r'[\w._%+\-]+@[\w.-]+\.[A-Za-z]{2,}'

if is_object(.attributes) {
  attrs, _err = object(.attributes)
  if is_object(attrs) {
    .attributes = map_values(object!(attrs)) -> |v| {
      if is_string(v) { replace(string!(v), email_re, "[REDACTED-EMAIL]") } else { v }
    }
  }
}

if is_object(.resource) {
  res, _err = object(.resource)
  if is_object(res) && is_object(res.attributes) {
    rattrs, _err2 = object(res.attributes)
    if is_object(rattrs) {
      .resource.attributes = map_values(object!(rattrs)) -> |v| {
        if is_string(v) { replace(string!(v), email_re, "[REDACTED-EMAIL]") } else { v }
      }
    }
  }
}

.


email_re = r'[\w._%+\-]+@[\w.-]+\.[A-Za-z]{2,}'

if is_object(.attributes) {
  attrs, _err = object(.attributes)
  if is_object(attrs) {
    .attributes = map_values(attrs) -> |v| {
      if is_string(v) { replace(string!(v), email_re, "[REDACTED-EMAIL]") } else { v }
    }
  }
}

if is_object(.resource) {
  res, _err = object(.resource)
  if is_object(res) && is_object(res.attributes) {
    rattrs, _err2 = object(res.attributes)
    if is_object(rattrs) {
      .resource.attributes = map_values(object!(rattrs)) -> |v| {
        if is_string(v) { replace(string!(v), email_re, "[REDACTED-EMAIL]") } else { v }
      }
    }
  }
}

.

-      .resource.attributes = map_values(object!(rattrs)) -> |v| {

+      .resource.attributes = map_values(rattrs) -> |v| {


The file /Users/steve.briggs/projects/IAC/terraform/internal-modules/kubernetes/monitoring/vector/tests/vrl/redact_emails_metrics/program.vrl has been updated. All occurrences were successfully replaced. (file state is current in your context — no need to Read it back)


Re-running all four VRL tests to confirm no regression.


cd /Users/steve.briggs/projects/IAC/terraform/internal-modules/kubernetes/monitoring/vector && for t in tests/vrl/*/run.sh; do echo "=== $t ==="; "$t" 2>&1 | tail -3; done

Re-run all four VRL tests


=== tests/vrl/drop_audit/run.sh ===
2026-05-05T20:27:26.892690Z  INFO vector::app: Log level is enabled. level="info"
drop_audit VRL OK
=== tests/vrl/redact_emails_logs/run.sh ===
2026-05-05T20:27:27.119635Z  INFO vector::app: Log level is enabled. level="info"
redact_emails_logs VRL OK
=== tests/vrl/redact_emails_metrics/run.sh ===
2026-05-05T20:27:27.321147Z  INFO vector::app: Log level is enabled. level="info"
redact_emails_metrics VRL OK
=== tests/vrl/redact_emails_traces/run.sh ===
2026-05-05T20:27:27.525125Z  INFO vector::app: Log level is enabled. level="info"
redact_emails_traces VRL OK


All 4 VRL tests still pass. Summary of fixes:

- **`templates/values.yaml.tpl`**: 5 instances of `map_values(object!(attrs))` / `map_values(object!(rattrs))` → `map_values(attrs)` / `map_values(rattrs)` across the three `redact_emails_*` transforms.
- **`tests/vrl/redact_emails_*/program.vrl`**: same 5 fixes applied to the standalone test programs so they stay in sync with the template.

**Root cause:** the `attrs, _err = object(.attributes)` pattern + the subsequent `if is_object(attrs) { ... }` guard already narrows `attrs` to a known object type. Wrapping it in `object!(attrs)` adds a fallibility-abort path that the compiler proves can never fire — Vector flags it with E620.

**Why it didn't show in unit tests:** `vector vrl --quiet` suppresses warnings (`-q` flag silences them), so the local test runner happily produced correct output without surfacing the warning. The deployed Vector logs at WARN level surface the same issue in the live config. Now suppressed in both places.

Re-deploy the helm release and the warning should be gone.


afplay ~/.claude/sounds/mario/stop/stomp.wav 2>/dev/null || true


I also need to update the cilium network policy to allow otel-collector to talk to the vector service (@/Users/steve.briggs/projects/IAC/terraform/internal-modules/kubernetes/monitoring/otel-collector/netpol.tf) can you do this?


1	locals {
2	  netpols = {
3	    "${var.name}" = local.metrics_server_netpol
4	  }
5	  metrics_server_netpol = {
6	    endpointSelector = {
7	      matchLabels = {
8	        "app.kubernetes.io/part-of" = "opentelemetry-collector"
9	      }
10	    }
11	    ingress = [
12	      {
13	        fromEntities = ["host"]
14	        toPorts = [{
15	          ports = [
16	            {
17	              port = "4317"
18	            },
19	            {
20	              port = "4318"
21	            },
22	            {
23	              port = "6831"
24	            }
25	          ]
26	        }]
27	      },
28	      {
29	        fromEntities = ["kube-apiserver"]
30	      },
31	      {
32	        fromEntities = ["cluster"]
33	        toPorts = [{
34	          ports = [
35	            {
36	              port = "4317"
37	            },
38	            {
39	              port = "4318"
40	            },
41	            {
42	              port = "6831"
43	            }
44	          ]
45	        }]
46	      },
47	      {
48	        fromEndpoints = [{}]
49	      }
50	    ]
51	    egress = [
52	      {
53	        toEntities = ["world"]
54	        toPorts = [{
55	          ports = [{
56	            port = "443"
57	          }]
58	        }]
59	      },
60	      {
61	        toEntities = ["kube-apiserver"]
62	      },
63	      {
64	        toEntities = ["cluster"]
65	      }
66	    ]
67	  }
68	}
69

locals {
  netpols = {
    "${var.name}" = local.metrics_server_netpol
  }
  metrics_server_netpol = {
    endpointSelector = {
      matchLabels = {
        "app.kubernetes.io/part-of" = "opentelemetry-collector"
      }
    }
    ingress = [
      {
        fromEntities = ["host"]
        toPorts = [{
          ports = [
            {
              port = "4317"
            },
            {
              port = "4318"
            },
            {
              port = "6831"
            }
          ]
        }]
      },
      {
        fromEntities = ["kube-apiserver"]
      },
      {
        fromEntities = ["cluster"]
        toPorts = [{
          ports = [
            {
              port = "4317"
            },
            {
              port = "4318"
            },
            {
              port = "6831"
            }
          ]
        }]
      },
      {
        fromEndpoints = [{}]
      }
    ]
    egress = [
      {
        toEntities = ["world"]
        toPorts = [{
          ports = [{
            port = "443"
          }]
        }]
      },
      {
        toEntities = ["kube-apiserver"]
      },
      {
        toEntities = ["cluster"]
      }
    ]
  }
}


Looking at the existing netpol — the egress rule `{ toEntities = ["cluster"] }` is broad enough that otel-collector can already talk to vector (no port restriction, no namespace scope). So the policy may already permit the traffic.

That said, adding an **explicit** rule for otel→vector is good practice (defense-in-depth, explicit > implicit, and someone tightening `cluster` egress later won't accidentally break this). Adding it now.


Edit

    egress = [
      {
        toEntities = ["world"]
        toPorts = [{
          ports = [{
            port = "443"
          }]
        }]
      },
      {
        toEntities = ["kube-apiserver"]
      },
      {
        toEntities = ["cluster"]
      }
    ]

    egress = [
      {
        toEntities = ["world"]
        toPorts = [{
          ports = [{
            port = "443"
          }]
        }]
      },
      {
        toEntities = ["kube-apiserver"]
      },
      {
        toEntities = ["cluster"]
      },
      {
        # Allow OTLP/gRPC to the Vector aggregator (when downstream_endpoint is set).
        toEndpoints = [{
          matchLabels = {
            "k8s:io.kubernetes.pod.namespace" = "vector"
            "app.kubernetes.io/name"          = "vector"
          }
        }]
        toPorts = [{
          ports = [
            {
              port = "4317"
            }
          ]
        }]
      }
    ]


The file /Users/steve.briggs/projects/IAC/terraform/internal-modules/kubernetes/monitoring/otel-collector/netpol.tf has been updated successfully. (file state is current in your context — no need to Read it back)

    egress = [
      {
        toEntities = ["world"]
        toPorts = [{
          ports = [{
            port = "443"
          }]
        }]
      },
      {
        toEntities = ["kube-apiserver"]
      },
      {
        toEntities = ["cluster"]
      },
      {
        # Allow OTLP/gRPC to the Vector aggregator (when downstream_endpoint is set).
        toEndpoints = [{
          matchLabels = {
            "k8s:io.kubernetes.pod.namespace" = "vector"
            "app.kubernetes.io/name"          = "vector"
          }
        }]
        toPorts = [{
          ports = [
            {
              port = "4317"
            }
          ]
        }]
      }
    ]

locals {
  netpols = {
    "${var.name}" = local.metrics_server_netpol
  }
  metrics_server_netpol = {
    endpointSelector = {
      matchLabels = {
        "app.kubernetes.io/part-of" = "opentelemetry-collector"
      }
    }
    ingress = [
      {
        fromEntities = ["host"]
        toPorts = [{
          ports = [
            {
              port = "4317"
            },
            {
              port = "4318"
            },
            {
              port = "6831"
            }
          ]
        }]
      },
      {
        fromEntities = ["kube-apiserver"]
      },
      {
        fromEntities = ["cluster"]
        toPorts = [{
          ports = [
            {
              port = "4317"
            },
            {
              port = "4318"
            },
            {
              port = "6831"
            }
          ]
        }]
      },
      {
        fromEndpoints = [{}]
      }
    ]
    egress = [
      {
        toEntities = ["world"]
        toPorts = [{
          ports = [{
            port = "443"
          }]
        }]
      },
      {
        toEntities = ["kube-apiserver"]
      },
      {
        toEntities = ["cluster"]
      }
    ]
  }
}

+        # Allow OTLP/gRPC to the Vector aggregator (when downstream_endpoint is set).

+            "k8s:io.kubernetes.pod.namespace" = "vector"

+            "app.kubernetes.io/name"          = "vector"


I also need to update the cilium network policy to allow otel-collector to talk to the vector service (@/Users/steve.briggs/projects/IAC/terraform/internal-modules/kubernetes/monitoring/otel-collector…


Bash

cd /Users/steve.briggs/projects/IAC/terraform/internal-modules/kubernetes/monitoring/otel-collector && mise exec terraform@1.9.0 -- terraform fmt -check && mise exec terraform@1.9.0 -- terraform validate 2>&1

Verify fmt + validate after netpol change


Success! The configuration is valid.


Done. `terraform fmt -check` and `terraform validate` both clean.

**What I added:** a fourth egress rule selecting Vector pods explicitly (namespace `vector`, label `app.kubernetes.io/name=vector`) on port `4317` (OTLP/gRPC).

**Why I left the existing rules alone:**
- `toEntities = ["cluster"]` — broad cluster-internal egress catch-all. Already permits otel→vector traffic. Removing it could break other intra-cluster flows we haven't surveyed (e.g., otel-lb→otel-collector-processor handoff via DNS) — out of scope for this change.
- `toEntities = ["world"]` on port 443 — was for direct-to-Datadog HTTPS. Becomes dead when `downstream_endpoint` is set (Vector handles egress now), but ripping it would tightly couple this netpol to the downstream-mode flag and complicate rollback. Leave it; it's harmless when unused.

**One thing worth flagging:** the Vector module itself doesn't ship a CiliumNetworkPolicy of its own. If your cluster has default-deny ingress for namespaces, otel-collector's egress permission isn't enough — Vector also needs an ingress rule allowing port 4317 from the otel-collector pods. If you hit "connection refused" or silent drops after deploying with this change, that's where to look next.


can you also use that example to create a netpol.tf for vector in @(/Users/steve.briggs/projects/IAC/terraform/internal-modules/kubernetes/monitoring/vector/netpol.tf) ?


Let me check how the otel-collector module actually consumes its `local.netpols` — there must be a netpol-creator module being called from `main.tf` that materializes the locals into Cilium CRDs.


Find netpol module call in otel-collector main.tf


294:module "netpol" {
297:  name             = join("-", [var.name, "netpol"])
299:  network_policies = local.netpols


290	    create_before_destroy = false
291	  }
292	}
293
294	module "netpol" {
295	  source = "git@gitlab.infrastructure.immuta.io:infrastructure/terraform/internal-modules/kubernetes/security/network-policy.git?ref=v1.1.0"
296
297	  name             = join("-", [var.name, "netpol"])
298	  namespace        = var.namespace
299	  network_policies = local.netpols
300
301	  tags = merge(var.tags, { "sourceModule" = "/internal-modules/kubernetes/monitoring/otel-collector" })
302
303	  depends_on = [
304	    kubernetes_namespace.this

    create_before_destroy = false
  }
}

module "netpol" {
  source = "git@gitlab.infrastructure.immuta.io:infrastructure/terraform/internal-modules/kubernetes/security/network-policy.git?ref=v1.1.0"

  name             = join("-", [var.name, "netpol"])
  namespace        = var.namespace
  network_policies = local.netpols

  tags = merge(var.tags, { "sourceModule" = "/internal-modules/kubernetes/monitoring/otel-collector" })

  depends_on = [
    kubernetes_namespace.this


grep -n "var.name\|var.namespace" /Users/steve.briggs/projects/IAC/terraform/internal-modules/kubernetes/monitoring/otel-collector/variables.tf | head -10

Check what var.name resolves to in otel-collector


(Bash completed with no output)


grep -rn 'variable "name"\|variable "namespace"' /Users/steve.briggs/projects/IAC/terraform/internal-modules/kubernetes/monitoring/otel-collector/ 2>&1; echo "---"; grep -n "^variable" /Users/steve.briggs/projects/IAC/terraform/internal-modules/kubernetes/monitoring/otel-collector/main.tf 2>&1 | head

Locate name/namespace variable defs


/Users/steve.briggs/projects/IAC/terraform/internal-modules/kubernetes/monitoring/otel-collector/variables.tf:19:variable "name" {
/Users/steve.briggs/projects/IAC/terraform/internal-modules/kubernetes/monitoring/otel-collector/variables.tf:78:variable "namespace" {
---


Got it. Pattern: `netpol.tf` declares `local.netpols` map → `main.tf` calls the shared `network-policy` module that materializes them as Cilium CRDs. Same shape for vector. Creating both pieces now.


Write

locals {
  netpols = {
    "vector" = local.vector_netpol
  }
  vector_netpol = {
    endpointSelector = {
      matchLabels = {
        "app.kubernetes.io/name" = "vector"
      }
    }
    ingress = [
      {
        # OTLP from any cluster-internal sender (primarily otel-collector).
        fromEntities = ["cluster"]
        toPorts = [{
          ports = [
            { port = "4317" },
            { port = "4318" },
          ]
        }]
      },
      {
        # Vector self-metrics scrape (Prometheus operator, Datadog Agent, etc.).
        fromEntities = ["cluster"]
        toPorts = [{
          ports = [{ port = "8686" }]
        }]
      },
      {
        # kubelet health probes from the node.
        fromEntities = ["host"]
        toPorts = [{
          ports = [
            { port = "4317" },
            { port = "4318" },
            { port = "8686" },
          ]
        }]
      },
      {
        fromEntities = ["kube-apiserver"]
      }
    ]
    egress = [
      {
        # Datadog SaaS HTTPS for logs/traces/metrics sinks.
        toEntities = ["world"]
        toPorts = [{
          ports = [{ port = "443" }]
        }]
      },
      {
        toEntities = ["kube-apiserver"]
      },
      {
        # DNS resolution + service discovery (CoreDNS lives here).
        toEntities = ["cluster"]
      }
    ]
  }
}


File created successfully at: /Users/steve.briggs/projects/IAC/terraform/internal-modules/kubernetes/monitoring/vector/netpol.tf (file state is current in your context — no need to Read it back)

locals {
  netpols = {
    "vector" = local.vector_netpol
  }
  vector_netpol = {
    endpointSelector = {
      matchLabels = {
        "app.kubernetes.io/name" = "vector"
      }
    }
    ingress = [
      {
        # OTLP from any cluster-internal sender (primarily otel-collector).
        fromEntities = ["cluster"]
        toPorts = [{
          ports = [
            { port = "4317" },
            { port = "4318" },
          ]
        }]
      },
      {
        # Vector self-metrics scrape (Prometheus operator, Datadog Agent, etc.).
        fromEntities = ["cluster"]
        toPorts = [{
          ports = [{ port = "8686" }]
        }]
      },
      {
        # kubelet health probes from the node.
        fromEntities = ["host"]
        toPorts = [{
          ports = [
            { port = "4317" },
            { port = "4318" },
            { port = "8686" },
          ]
        }]
      },
      {
        fromEntities = ["kube-apiserver"]
      }
    ]
    egress = [
      {
        # Datadog SaaS HTTPS for logs/traces/metrics sinks.
        toEntities = ["world"]
        toPorts = [{
          ports = [{ port = "443" }]
        }]
      },
      {
        toEntities = ["kube-apiserver"]
      },
      {
        # DNS resolution + service discovery (CoreDNS lives here).
        toEntities = ["cluster"]
      }
    ]
  }
}


Now wiring the netpol module call into `main.tf`.


Edit

  depends_on = [
    kubernetes_manifest.datadog_external_secret,
  ]
}

  depends_on = [
    kubernetes_manifest.datadog_external_secret,
  ]
}

module "netpol" {
  source = "git@gitlab.infrastructure.immuta.io:infrastructure/terraform/internal-modules/kubernetes/security/network-policy.git?ref=v1.1.0"

  name             = "vector-netpol"
  namespace        = var.namespace
  network_policies = local.netpols

  depends_on = [
    kubernetes_namespace_v1.vector,
  ]
}


<tool_use_error>File has been modified since read, either by the user or by a linter. Read it again before attempting to write it.</tool_use_error>

Error: File has been modified since read, either by the user or by a linter. Read it again before attempting to write it.


75	resource "helm_release" "vector" {
76	  name       = "vector"
77	  namespace  = kubernetes_namespace_v1.vector.metadata[0].name
78	  repository = "https://helm.vector.dev"
79	  chart      = "vector"
80	  version    = var.chart_version
81
82	  values = [local.rendered_values]
83
84	  set {
85	    name  = "image.repository"
86	    value = var.image
87	  }
88	  set {
89	    name  = "image.tag"
90	    value = var.image_tag_version
91	  }
92
93	  depends_on = [
94	    kubernetes_manifest.datadog_external_secret,
95	  ]
96	}
97

resource "helm_release" "vector" {
  name       = "vector"
  namespace  = kubernetes_namespace_v1.vector.metadata[0].name
  repository = "https://helm.vector.dev"
  chart      = "vector"
  version    = var.chart_version

  values = [local.rendered_values]

  set {
    name  = "image.repository"
    value = var.image
  }
  set {
    name  = "image.tag"
    value = var.image_tag_version
  }

  depends_on = [
    kubernetes_manifest.datadog_external_secret,
  ]
}


  depends_on = [
    kubernetes_manifest.datadog_external_secret,
  ]
}

  depends_on = [
    kubernetes_manifest.datadog_external_secret,
  ]
}

module "netpol" {
  source = "git@gitlab.infrastructure.immuta.io:infrastructure/terraform/internal-modules/kubernetes/security/network-policy.git?ref=v1.1.0"

  name             = "vector-netpol"
  namespace        = var.namespace
  network_policies = local.netpols

  depends_on = [
    kubernetes_namespace_v1.vector,
  ]
}


The file /Users/steve.briggs/projects/IAC/terraform/internal-modules/kubernetes/monitoring/vector/main.tf has been updated successfully. (file state is current in your context — no need to Read it back)

  depends_on = [
    kubernetes_manifest.datadog_external_secret,
  ]
}

module "netpol" {
  source = "git@gitlab.infrastructure.immuta.io:infrastructure/terraform/internal-modules/kubernetes/security/network-policy.git?ref=v1.1.0"

  name             = "vector-netpol"
  namespace        = var.namespace
  network_policies = local.netpols

  depends_on = [
    kubernetes_namespace_v1.vector,
  ]
}

locals {
  audit_drop_condition = length(var.audit_drop_patterns) == 0 ? "true" : join(
    " && ",
    [for p in var.audit_drop_patterns : "!match(msg, r'${p}')"],
  )

  rendered_values = var.additional_config != null ? yamlencode({ customConfig = var.additional_config }) : templatefile(
    "${path.module}/templates/values.yaml.tpl",
    {
      replicas                    = var.replicas
      requests_cpu                = var.resources_requests.cpu
      requests_memory             = var.resources_requests.memory
      limits_cpu                  = var.resources_limits.cpu
      limits_memory               = var.resources_limits.memory
      datadog_site                = var.datadog_site
      audit_drop_condition        = local.audit_drop_condition
      email_redaction_enabled     = var.email_redaction_enabled
      email_redaction_replacement = var.email_redaction_replacement
      service_monitor_enabled     = var.service_monitor_enabled
    },
  )
}

resource "kubernetes_namespace_v1" "vector" {
  metadata {
    name = var.namespace
    labels = {
      "app.kubernetes.io/name"      = "vector"
      "app.kubernetes.io/component" = "telemetry-aggregator"
    }
  }
}

resource "kubernetes_manifest" "datadog_external_secret" {
  manifest = {
    apiVersion = "external-secrets.io/v1"
    kind       = "ExternalSecret"
    metadata = {
      name      = "vector-datadog"
      namespace = kubernetes_namespace_v1.vector.metadata[0].name
    }
    spec = {
      refreshInterval = "1h"
      secretStoreRef = {
        kind = "ClusterSecretStore"
        name = var.external_secrets_cluster_store_name
      }
      target = {
        name           = "vector-datadog"
        creationPolicy = "Owner"
      }
      data = [{
        secretKey = "api_key"
        remoteRef = {
          key      = var.datadog_api_key_vault_path
          property = "apiKey"
        }
      }]
    }
  }

  wait {
    fields = {
      "status.conditions[0].type"   = "Ready"
      "status.conditions[0].status" = "True"
    }
  }

  timeouts {
    create = "5m"
    update = "5m"
  }
}

resource "helm_release" "vector" {
  name       = "vector"
  namespace  = kubernetes_namespace_v1.vector.metadata[0].name
  repository = "https://helm.vector.dev"
  chart      = "vector"
  version    = var.chart_version

  values = [local.rendered_values]

  set {
    name  = "image.repository"
    value = var.image
  }
  set {
    name  = "image.tag"
    value = var.image_tag_version
  }

  depends_on = [
    kubernetes_manifest.datadog_external_secret,
  ]
}

+  source = "git@gitlab.infrastructure.immuta.io:infrastructure/terraform/internal-modules/kubernetes/security/network-policy.git?ref=v1.1.0"


Bash

cd /Users/steve.briggs/projects/IAC/terraform/internal-modules/kubernetes/monitoring/vector && mise exec terraform@1.9.0 -- terraform fmt -check -recursive && mise exec terraform@1.9.0 -- terraform init -backend=false 2>&1 | tail -5 && mise exec terraform@1.9.0 -- terraform validate 2>&1

Verify fmt + init + validate after netpol additions


Error: Exit code 1
│ Could not retrieve the list of available versions for provider
│ hashicorp/helm: locked provider registry.terraform.io/hashicorp/helm 3.1.1
│ does not match configured version constraint ~> 2.0, >= 2.10.0; must use
│ terraform init -upgrade to allow selection of new versions
╵
╷
│ Error: Unsupported block type
│
│   on main.tf line 84, in resource "helm_release" "vector":
│   84:   set {
│
│ Blocks of type "set" are not expected here. Did you mean to define argument
│ "set"? If so, use the equals sign to assign it a value.
╵
╷
│ Error: Unsupported block type
│
│   on main.tf line 88, in resource "helm_release" "vector":
│   88:   set {
│
│ Blocks of type "set" are not expected here. Did you mean to define argument
│ "set"? If so, use the equals sign to assign it a value.
╵


Provider lockfile conflict — the network-policy module pulls in a `~> 2.0` helm constraint that conflicts with the v3.1.1 currently locked in `.terraform.lock.hcl` (from Phase A's earlier validate). Need to upgrade the lockfile to let Terraform pick a v2.x that satisfies all constraints.


cd /Users/steve.briggs/projects/IAC/terraform/internal-modules/kubernetes/monitoring/vector && mise exec terraform@1.9.0 -- terraform init -backend=false -upgrade 2>&1 | tail -15 && echo "===VALIDATE===" && mise exec terraform@1.9.0 -- terraform validate 2>&1

Refresh provider lock to satisfy network-policy module constraints


- Installing hashicorp/helm v2.17.0...
- Installed hashicorp/helm v2.17.0 (signed by HashiCorp)
Terraform has made some changes to the provider dependency selections recorded
in the .terraform.lock.hcl file. Review those changes and commit them to your
version control system if they represent changes you intended to make.

Terraform has been successfully initialized!

You may now begin working with Terraform. Try running "terraform plan" to see
any changes that are required for your infrastructure. All Terraform commands
should now work.

If you ever set or change modules or backend configuration for Terraform,
rerun this command to reinitialize your working directory. If you forget, other
commands will detect it and remind you to do so if necessary.
===VALIDATE===
Success! The configuration is valid.


Helm v2.17.0 now locked, validate clean.

**Summary of changes:**

- **`vector/netpol.tf` (new)** — declares `local.netpols` with one CiliumNetworkPolicy named `vector`:
  - **Ingress**: `cluster` on 4317/4318 (OTLP from any pod, including otel-collector); `cluster` on 8686 (Vector self-metrics scrape); `host` on 4317/4318/8686 (kubelet probes); `kube-apiserver` (webhook compatibility, mirrors otel-collector pattern).
  - **Egress**: `world:443` (Datadog SaaS); `kube-apiserver`; `cluster` (DNS/CoreDNS + service discovery).
- **`vector/main.tf`** — added a `module "netpol"` call pointing at the same shared `internal-modules/kubernetes/security/network-policy.git?ref=v1.1.0` module that otel-collector uses, with `name = "vector-netpol"`, `namespace = var.namespace`, `network_policies = local.netpols`, depending on the namespace resource.
- **`.terraform.lock.hcl`** — auto-regenerated to lock helm provider at v2.17.0 (the network-policy module's `~> 2.0` constraint forced this). This is the right version for your `eks-cluster-applications` consumer too — keeps the helm provider major aligned across the stack.

Things I deliberately left out of the netpol but worth flagging:

- **Egress to `external-secrets-operator` for the ExternalSecret reconcile** isn't needed — ESO operates in its own namespace and writes the K8s Secret out-of-band; Vector pods just env-mount it. No live ESO traffic from Vector.
- **Egress scoping to specific Datadog FQDNs** (`*.datadoghq.com`, etc.) via Cilium's `toFQDNs` would be tighter than `world:443` but requires DNS visibility configured cluster-wide. Mirroring otel-collector's `world:443` pattern keeps it consistent for now — tighten later if compliance asks.
- **Per-tier scoping** — both former otel-collector tiers (otel-lb and processor) match the same netpol because the selector is `app.kubernetes.io/part-of=opentelemetry-collector`. Vector is single-tier so that complication doesn't apply here.


what is a good simple way i can test the log redaction in the cluster adhoc?


Simplest end-to-end test: send a known OTLP log via curl from inside the cluster, then search Datadog for the marker UUID. Three test cases — email redact, audit-drop pattern A, audit-drop pattern B.

### Setup — find the OTel collector OTLP/HTTP endpoint

```bash
kubectl get svc -A | grep -i otel
# look for the LB-tier service; default DNS will be:
# otel-collector-lb.<otel-namespace>.svc.cluster.local on port 4318
```

### Test 1 — email redaction

```bash
UUID=$(uuidgen)
kubectl run -i --rm otel-test --image=curlimages/curl --restart=Never -- \
  curl -sS -X POST http://otel-collector-lb.monitoring.svc.cluster.local:4318/v1/logs \
    -H "Content-Type: application/json" \
    -d "{
      \"resourceLogs\": [{
        \"resource\": {\"attributes\": [{\"key\": \"service.name\", \"value\": {\"stringValue\": \"redaction-test\"}}]},
        \"scopeLogs\": [{\"logRecords\": [{
          \"timeUnixNano\": \"$(date +%s)000000000\",
          \"severityText\": \"INFO\",
          \"body\": {\"stringValue\": \"e2e-${UUID}: contact alice@example.com for help\"}
        }]}]
      }]
    }"

echo "Datadog query: e2e-${UUID}"
```

**Pass**: log appears in Datadog within ~30s; body shows `contact [REDACTED-EMAIL] for help`. The string `alice@example.com` does **not** appear anywhere.

### Test 2 — audit-drop pattern A (`Audit - ...`)

Same curl pattern, body changes:

```bash
\"body\": {\"stringValue\": \"Audit - test-${UUID} login event\"}
```

**Pass**: Datadog search for the UUID returns **zero results** — the log was filtered before reaching Datadog.

### Test 3 — audit-drop pattern B (`"level":"audit"`)

```bash
\"body\": {\"stringValue\": \"{\\\"level\\\":\\\"audit\\\",\\\"event\\\":\\\"test-${UUID}\\\"}\"}
```

**Pass**: same — zero Datadog results for the UUID.

### If you want to test Vector in isolation (skipping otel-collector)

Useful for debugging whether the issue is in OTel-side or Vector-side. POST OTLP/HTTP directly to Vector's service:

```bash
kubectl run -i --rm vector-test --image=curlimages/curl --restart=Never -n vector -- \
  curl -sS -X POST http://vector.vector.svc.cluster.local:4318/v1/logs \
    -H "Content-Type: application/json" \
    -d '<same payload as Test 1>'
```

If the redaction works here but not via the otel-collector path, the regression is in OTel's exporter config or the netpol between them.

### Watch Vector's discard counter while you test

Tail Vector's metrics endpoint to see the audit-drop events get counted in real time:

```bash
kubectl exec -n vector deploy/vector -- curl -s http://localhost:8686/metrics | grep -E "vector_component_(discarded|sent)_events_total"
```

Run Test 2 and Test 3 a few times — you should see `vector_component_discarded_events_total{component_id="drop_audit"}` tick up while `vector_component_sent_events_total{component_id="datadog_logs"}` does not.


You've built the Vector + OTel redaction pipeline end-to-end and it's deployed; right now you're verifying redaction in-cluster. Next: run Test 1 (the curl pod sending an OTLP log with an email) and confirm Datadog shows `[REDACTED-EMAIL]`.


<command-message>obsidian-save</command-message>
<command-name>/obsidian-save</command-name>


Save current conversation context to an Obsidian note under the `Claude/` directory.

## What to save

Capture all of the following that are relevant to the current conversation:

- **Conversation summary**: What the user is trying to accomplish and why
- **Project context**: Relevant architecture, design decisions, or constraints discovered
- **Task state**: What has been completed, what is in progress, what is blocked or pending
- **Key decisions**: Any choices made and the reasoning behind them
- **Open questions**: Unresolved questions or things to revisit
- **Next steps**: Concrete actions that should follow from this session

## File location

Derive the project name from the current working directory (use the repo or app folder name). Save to:

```
Claude/<project-name>/<YYYY-MM-DD>-<short-slug>.md
```

Use today's date and a short 2-4 word slug describing the session topic (e.g. `2026-04-29-ruth-plugin-setup.md`).

If `` is provided, use it as the slug instead of generating one.

## Note format

Use this structure:

```markdown
---
date: <YYYY-MM-DD>
project: <project-name>
tags: [claude-context]
---

# <Session Title>

## Summary
<1-3 sentence overview of what this session was about>

## Context
<Relevant project background, architecture notes, constraints>

## Decisions
<Decisions made and rationale>

## Task State
<What was completed, in progress, or blocked>

## Open Questions
<Anything unresolved>

## Next Steps
<Concrete follow-up actions>
```

## Steps

1. Determine the project name from the working directory.
2. Determine today's date.
3. Draft the note content based on the current conversation.
4. Use `mcp__obsidian__create_vault_file` to write the note to the vault.
5. Confirm to the user where the note was saved.


---
date: 2026-05-05
project: IAC
tags: [claude-context]
---

# Vector deploy fixes + netpol additions

## Summary

Post-implementation iteration session. Fixed three deployment-blocking issues that surfaced once Sir Charles started actually deploying the Vector module: helm provider v2/v3 syntax mismatch, a VRL E620 warning from over-narrowed `object!()` calls, and missing CiliumNetworkPolicy coverage for the new vector→Datadog and otel-collector→vector flows. Capped with an ad-hoc test methodology for verifying redaction in a live cluster.

Companion note: `2026-05-05-vector-redaction-pipeline.md` — the original design + implementation session.

## Context

**Pipeline state (post-rollout):** `apps → otel-collector → vector → Datadog`. Vector module deployed locally at `/Users/steve.briggs/projects/IAC/terraform/internal-modules/kubernetes/monitoring/vector/`. otel-collector module bumped (work-in-progress on `otel-batch-size` branch ahead of v1.0.5; needs Sir Charles to handle the rebase to a clean v1.1.0 release branch). Consumer wired in `eks-cluster-applications/vector.tf`.

**Deployment environment provider matrix:**
- Sir Charles's `eks-cluster-applications` consumer pins kubernetes/helm providers to v2.x (kubernetes provider via lockfile, helm provider v2.17.0).
- The implementer subagents had v3.1.0/v3.1.1 resolved in their sandboxes (no lockfile, `>= 2.10` constraint allows v3).
- Result: subagent-authored code used v3 syntax (`set = [{}]` list, `kubernetes = { ... }` equals-block) which broke v2-resolution at deploy time.

**Vector 0.55 VRL stricture:** the compiler narrows types after `if is_object(x)` guards, so subsequently calling `object!(x)` (the abort-on-fail variant) is rejected with E620 — the abort path can never fire. Same for `string!()` on already-string-typed locals.

## Decisions

**1. Use helm v2 syntax in the vector module.** The consumer dictates the resolved version. Reverted `vector/main.tf` `set = [{...}]` → `set { name = ..., value = ... }`. Also fixed `vector/examples/basic/versions.tf` provider helm block (`kubernetes = { ... }` → `kubernetes { ... }`).

**Did NOT tighten `versions.tf` constraint** to `~> 2.10`. Leaving `>= 2.10` keeps the door open for future v3 callers; the syntax fixes work in v2 only, but if Immuta ever upgrades to helm v3 we'll need to revisit syntax anyway. Loose constraint = easier future migration.

**2. Drop the unnecessary `object!()` wrappers from VRL.** Pattern was:
```vrl
if is_object(.attributes) {
  attrs, _err = object(.attributes)
  if is_object(attrs) {
    .attributes = map_values(object!(attrs)) -> |v| { ... }
  }
}
```

The `object!(attrs)` is redundant — after the inner `is_object(attrs)` guard, attrs is narrowed. Just pass `attrs` directly: `map_values(attrs) -> |v| { ... }`.

Applied to all 5 instances in `templates/values.yaml.tpl` and matching test programs (`tests/vrl/redact_emails_*/program.vrl`). All 4 VRL tests still pass.

**3. Explicit egress rule for otel-collector → vector, additive only.**

Added a fourth egress rule to `otel-collector/netpol.tf` selecting Vector pods explicitly:
```hcl
{
  toEndpoints = [{
    matchLabels = {
      "k8s:io.kubernetes.pod.namespace" = "vector"
      "app.kubernetes.io/name"          = "vector"
    }
  }]
  toPorts = [{ ports = [{ port = "4317" }] }]
}
```

**Did NOT remove the existing rules.** The pre-existing `toEntities = ["cluster"]` egress (no port restriction) already permits this traffic; the new rule is defense-in-depth. The `toEntities = ["world"]` on port 443 (formerly direct-to-Datadog HTTPS) is dead weight in downstream mode but stays put — ripping it would couple the netpol to the downstream-mode flag and complicate rollback.

**4. Mirror otel-collector's netpol pattern for the new vector module netpol.**

Vector module had no netpol. Created `vector/netpol.tf` declaring `local.netpols` and a `module "netpol"` call in `vector/main.tf` pointing at the shared `internal-modules/kubernetes/security/network-policy.git?ref=v1.1.0` module. Same call signature as otel-collector — `name = "vector-netpol"`, `namespace = var.namespace`, `network_policies = local.netpols`, depends_on the namespace resource.

Vector netpol surface:
- **Ingress**: `cluster` on 4317/4318 (OTLP from any pod incl. otel-collector); `cluster` on 8686 (self-metrics scrape — Prometheus operator, Datadog Agent OpenMetrics, etc.); `host` on 4317/4318/8686 (kubelet probes); `kube-apiserver` (webhook compatibility, mirroring otel-collector pattern).
- **Egress**: `world:443` (Datadog SaaS sinks); `kube-apiserver`; `cluster` (DNS via CoreDNS + intra-cluster discovery).

**5. Skipped ESO egress from Vector pods.** External Secrets Operator writes the K8s Secret out-of-band; Vector pods just env-mount the resulting Secret. No live ESO traffic from Vector to surface in netpol.

## Task State

**Completed in this session:**

- `vector/main.tf` line 84-93: helm v3 list syntax → v2 block syntax for `set` directives.
- `vector/examples/basic/versions.tf` line 14: helm v3 `kubernetes = { ... }` → v2 `kubernetes { ... }` block.
- `vector/templates/values.yaml.tpl`: 5 instances of `map_values(object!(attrs))` and `map_values(object!(rattrs))` → unwrapped form. Removes the E620 compile warnings at deploy time.
- `vector/tests/vrl/redact_emails_logs/program.vrl`, `redact_emails_traces/program.vrl`, `redact_emails_metrics/program.vrl`: same 5 unwrappings, kept in sync with template. All 4 VRL tests still pass.
- `otel-collector/netpol.tf`: added 4th egress rule for explicit otel→vector permission on port 4317. `terraform validate` clean.
- `vector/netpol.tf` (new): declared `local.netpols` map for Vector's CiliumNetworkPolicy with ingress/egress as documented above.
- `vector/main.tf`: added `module "netpol"` call wiring the netpol locals into the shared network-policy module. depends_on the namespace.
- `vector/.terraform.lock.hcl`: regenerated via `terraform init -upgrade`. Helm provider now locked at `v2.17.0` (the network-policy module's transitive `~> 2.0` constraint forced this). `terraform validate` clean.

**Memory file written earlier (still relevant):** `feedback_platform_components_use_terraform.md` in `~/.claude/projects/-Users-steve-briggs-projects-IAC/memory/`.

**Not yet done (still Sir Charles's responsibility, unchanged from prior note):**
- Tag `vector v1.0.0` in GitLab.
- Tag `otel-collector v1.1.0` (after rebase decision on the `otel-batch-size` branch perf-tuning entanglement).
- Open MRs for all three repos.
- Run `terraform plan` against a real cluster instance directory (Task C3, deferred).

## Open Questions

1. **Real cluster log path.** Spec confirmed apps→OTel→Datadog is the funnel, but it's unclear what component tails container stdout and forwards via OTLP to otel-collector. Could be Datadog Agent in OTLP-forwarding mode, a separate filelog-receiver DaemonSet, or app SDKs emitting OTLP. Doesn't affect redaction correctness (anything that reaches otel-collector gets redacted) but matters for understanding test coverage during rollout.

2. **The `set` syntax fix is one-way.** If Immuta ever upgrades helm provider to v3, the v2 block syntax in `vector/main.tf` will break with the inverse error. Document in the module README's "Compatibility" section as a known limitation pending v3 migration.

3. **`world:443` egress narrowing.** Both otel-collector (now dead-weight in downstream mode) and vector use generic `world:443` for Datadog egress. Could tighten to `toFQDNs = [*.datadoghq.com, *.us3.datadoghq.com, ...]` if Cilium DNS visibility is configured cluster-wide. Tightening hooks: ask compliance if/when defense-in-depth at FQDN level matters.

4. **otel-collector egress catch-all.** The pre-existing `toEntities = ["cluster"]` egress with no port restriction is broad. If Immuta later tightens this, ensure the explicit vector egress rule we added survives the cleanup — it's the only thing then permitting otel→vector traffic.

## Next Steps

**Verifying redaction works end-to-end (ad-hoc method documented to user):**

1. Find the otel-collector OTLP/HTTP service endpoint:
   ```bash
   kubectl get svc -A | grep -i otel
   ```
   Default DNS likely: `otel-collector-lb.<namespace>.svc.cluster.local:4318`.

2. Send three OTLP/HTTP test logs from a one-shot curl pod with a UUID marker:
   - **Email redact**: body `e2e-<UUID>: contact alice@example.com for help` — expect Datadog log with `[REDACTED-EMAIL]` substitution.
   - **Audit drop A**: body `Audit - test-<UUID>` — expect ZERO Datadog results.
   - **Audit drop B**: body `{"level":"audit","event":"test-<UUID>"}` — expect ZERO results.

3. Watch Vector's discard counter during the test:
   ```bash
   kubectl exec -n vector deploy/vector -- curl -s http://localhost:8686/metrics | grep -E "vector_component_(discarded|sent)_events_total"
   ```
   Audit-drop events should tick `discarded_events_total{component_id="drop_audit"}` up; `sent_events_total{component_id="datadog_logs"}` should NOT increase for the dropped messages.

4. **Vector-in-isolation test** (debugging only) — POST OTLP directly to `vector.vector.svc.cluster.local:4318` to bisect "is it OTel-side or Vector-side" if the e2e tests fail.

**Operational follow-ups (carried over):**

5. Author runbook entries: Vector pod debug, queue-full triage, key rotation behavior under reloader, rollback to direct-Datadog (set `vector_enabled = false`).
6. Wire Datadog monitors per spec §6.3 — particularly `vector_component_errors_total{sink=~"datadog_.*"}` rate alerting.
7. Soak the rollout in `mgmt` for 3-7 days before promoting to `cps`/`saas` (per spec §7.4).
8. Decommission `kubernetes/applications/datadog-processing-rules/` after one quarter clean-running in prod.

**Future hardening (post-v1):**

9. Add `output "cluster_secret_store_name"` to external-secrets internal-module; replace `var.vector_external_secrets_cluster_store_name` workaround.
10. Tighten `toEntities = ["world"]` egress to `toFQDNs` for Datadog if compliance asks.
11. Replace `data "external" "vector_validate"` brittle pattern with `lifecycle { precondition }` block.


File created successfully

File created successfully


Saved to `Claude/IAC/2026-05-05-vector-deploy-fixes.md` in your Obsidian vault, Sir Charles of St Cheese. Companion to the earlier `2026-05-05-vector-redaction-pipeline.md` from the planning/implementation session — that one captures design context, this one captures the post-deployment iteration work (helm v2/v3 fix, VRL E620 unwrapping, two netpol additions, lockfile regen) plus the ad-hoc redaction test methodology.


Building a Vector + OTel redaction pipeline for EKS clusters; module code, netpols, and ad-hoc test recipe are all in place locally. Next action is for you to handle git (tag vector v1.0.0, otel-collector v1.1.0, open MRs).