ObservabilityXXXVI · Log ShippingLogShipping
Collector Security
What you'll learn
- Configure TLS and basic_auth for the Loki exporter and verify the certificate chain
- Manage collector secrets via environment and file references, never as literals in the committed config
- Apply redaction at the source (Alloy stage.replace or OTel transform) so secrets never leave the host
- Establish an audit trail of collector configuration changes and write events
Prerequisites
Verified against Prometheus 2.55.x · Alertmanager 0.28.x · node_exporter 1.8.x · blackbox_exporter 0.26.x · Grafana 11.x · Loki 3.x · Tempo current · OpenTelemetry Collector 0.110.x · Grafana Alloy current · Docker Engine 28.x · Ubuntu 24.04 LTS · Debian 12 (Bookworm) · RHEL / Rocky / AlmaLinux 9.x · 2026-08-13
A security review finds that the observability fleet ships
unredacted credit-card numbers to Loki. The application writes
request_body to its log lines; the Alloy pipeline forwards
them as-is. The fix is a stage.replace upstream of the
loki.write that masks the card number before the line leaves
the host. The audit trail for the fix is the config diff in
the Git repository and the timestamp on the new Alloy image.
This lesson is the security model of the collector: TLS, secrets management, redaction at the source, mTLS, and the audit trail.
What it is
The collector is a network service. It accepts data on listening ports, ships data to backends, holds credentials, and reads host files. Each surface has a security shape; the config is where the shape is enforced.
The threat model has five components.
- Confidentiality in transit. Telemetry must travel encrypted between the collector and the backends. Unencrypted telemetry is sniffable on every hop.
- Authentication of the collector to the backend. The backend must verify that the collector is who it claims to be. Unauthenticated ingest is an open ingest surface.
- Authentication of the backend to the collector. The collector must verify that the backend is who it claims to be. An attacker that can MITM the connection can inject fake log lines.
- Confidentiality of credentials. The collector’s credentials (passwords, tokens) must not be visible in committed config files, in process arguments, or in logs.
- Confidentiality of payload. Sensitive fields in log lines (credit cards, passwords, personal data) must be redacted before the line leaves the host.
The collector configuration is the surface where the threat model is enforced. Each component has a TLS block, a credentials block, and a redaction surface.
Why a sysadmin cares
Five failure shapes appear when the threat model is not enforced.
- The plaintext password in the committed config. A team committed a Loki basic_auth password to the Alloy config in the platform repository. The password was in version control; the password was in the Git history; the password rotated every quarter because the previous quarter’s password had been leaked in a pull request.
- The redaction that ran after the writer. A team added
a regex redaction to the Loki
derived_fieldsconfig, expecting it to mask credit-card numbers. The redaction ran query-side; the unmasked lines had already been written to Loki’s chunks. The data was in the chunks; the redaction was cosmetic. - The collector that accepted any certificate. A team
set
tls.insecure_skip_verify: truebecause the CA bundle was missing. The connection succeeded; the lines shipped; nobody noticed until the certificate expired and the collector silently stopped shipping. Worse: an attacker that could MITM the connection could inject fake log lines. - The audit trail that was the collector’s stdout. A team relied on the collector’s own log output as the audit trail. The collector logs included the password on a connection failure; the password was in the agent’s log file; the log file was in a public S3 bucket.
- The mTLS that was TLS-only. A team configured TLS for the gateway’s receiver but not for the agents. The agents shipped to the gateway over plaintext within the cluster network. An attacker on the cluster network could inject fake log lines.
How it works
TLS to the backend
The collector’s exporters connect to backends over TLS. The TLS block has three components.
ca_file- the path to the CA bundle that signs the backend’s certificate. The default is the system trust store; in production, mount the bundle from a ConfigMap or the host’s trust store.cert_fileandkey_file- the client certificate and key for mutual TLS (mTLS). The backend must have the CA that signed this certificate in its trust store.insecure_skip_verify- a development-only flag that disables certificate verification. Never set in production.
// Alloy: loki.write with TLS
loki.write "loki" {
endpoint {
url = "https://loki.internal.example.com/loki/api/v1/push"
tenant_id = "prod"
basic_auth {
username = "ingest"
password_file = "/etc/alloy/secrets/loki-pass"
}
tls {
ca_file = "/etc/alloy/certs/ca.pem"
cert_file = "/etc/alloy/certs/client.pem"
key_file = "/etc/alloy/certs/client-key.pem"
}
}
}
# OTel Collector: loki exporter with TLS
exporters:
loki:
endpoint: https://loki.internal.example.com/loki/api/v1/push
default_labels_enabled: true
headers:
X-Scope-OrgID: prod
Authorization: Basic ${env:LOKI_BASIC_AUTH}
tls:
ca_file: /etc/otelcol/certs/ca.pem
cert_file: /etc/otelcol/certs/client.pem
key_file: /etc/otelcol/certs/client-key.pem
sending_queue:
enabled: true
Secrets management
Secrets must not appear as literals in the committed config. The supported patterns are environment variables and files.
- Environment variables.
${env:VAR}in OTel YAML;env("VAR")in River. The variable must be present in the process environment at start time. In Kubernetes, set the variable from aSecretviaenvFrom. - Files.
${file:/path}in OTel YAML;file("/path")in River. The file must be readable by the process and not readable by other users. In Kubernetes, mount theSecretas a file viavolumes. - Vaults and secret managers. External secret managers (HashiCorp Vault, AWS Secrets Manager, Kubernetes Secrets) inject the secret into the process environment or into a file. The collector does not speak Vault natively; the integration is at the platform layer.
A literal in the committed config is a credential leak. The file is in version control; the secret is shared; the secret must be rotated.
Redaction at the source
Sensitive fields must be redacted before the line leaves the host. The redaction surface is the pipeline upstream of the exporter.
In Alloy, the stage.replace stage rewrites the entry line
with a regex substitution.
loki.process "redact" {
// Mask credit-card numbers in the request_body field.
stage.replace {
expression = "(\\d{4}[- ]?\\d{4}[- ]?\\d{4}[- ]?\\d{4})"
replace = "[REDACTED_CC]"
}
// Mask JWT tokens.
stage.replace {
expression = "eyJ[A-Za-z0-9_-]+\\.eyJ[A-Za-z0-9_-]+\\.[A-Za-z0-9_-]+"
replace = "[REDACTED_JWT]"
}
forward_to = [loki.write.loki.receiver]
}
In the OTel Collector, the transform processor applies a
small DSL to entries.
processors:
transform:
error_mode: ignore
log_statements:
- context: log
statements:
- replace_pattern(body, "(\\d{4}[- ]?\\d{4}[- ]?\\d{4}[- ]?\\d{4})", "[REDACTED_CC]")
- replace_pattern(body, "eyJ[A-Za-z0-9_-]+\\.eyJ[A-Za-z0-9_-]+\\.[A-Za-z0-9_-]+", "[REDACTED_JWT]")
The redaction must run before the exporter. A redaction
that runs query-side (in Loki’s derived_fields or in a
Grafana transformation) does not protect the chunks; the
unmasked lines are already in storage.
mTLS between collectors
When agents ship to a central gateway, the link between agent and gateway can be encrypted with mTLS. The gateway’s receiver requires a client certificate; the agent presents its certificate; both sides verify the chain.
# Gateway: otlp receiver with mTLS
receivers:
otlp:
protocols:
grpc:
endpoint: 0.0.0.0:4317
tls:
cert_file: /etc/otelcol/certs/gateway.pem
key_file: /etc/otelcol/certs/gateway-key.pem
client_ca_file: /etc/otelcol/certs/agent-ca.pem
# Require client certs.
require_client_cert: true
# Agent: otlp exporter with mTLS
exporters:
otlp:
endpoint: otelcol-gateway.observability.svc.cluster.local:4317
tls:
ca_file: /etc/otelcol/certs/gateway-ca.pem
cert_file: /etc/otelcol/certs/agent.pem
key_file: /etc/otelcol/certs/agent-key.pem
The client_ca_file is the CA that signs the agent
certificates; the gateway uses it to verify the agent’s
client certificate. The require_client_cert: true flag
makes the verification mandatory.
How to configure it
A complete annotated Alloy config that demonstrates TLS, secrets management, and redaction.
// /etc/alloy/config.alloy
logging {
level = "info"
format = "logfmt"
}
// 1. Source.
loki.source.file "app" {
targets = [{
__path__ = "/var/log/app/*.log",
job = "checkout",
host = constants.hostname,
}]
forward_to = [loki.process.app.receiver]
}
// 2. Redact before any other processing.
loki.process "app" {
// Credit-card redaction.
stage.replace {
expression = "(\\d{4}[- ]?\\d{4}[- ]?\\d{4}[- ]?\\d{4})"
replace = "[REDACTED_CC]"
}
// JWT redaction.
stage.replace {
expression = "eyJ[A-Za-z0-9_-]+\\.eyJ[A-Za-z0-9_-]+\\.[A-Za-z0-9_-]+"
replace = "[REDACTED_JWT]"
}
// Promote the level label after redaction.
stage.regex {
expression = "^(?P<ts>\\S+) (?P<level>\\S+) (?P<msg>.*)$"
}
stage.labels {
values = { level = "" }
}
forward_to = [loki.write.loki.receiver]
}
// 3. Write with TLS and secrets from a file.
loki.write "loki" {
endpoint {
url = "https://loki.internal.example.com/loki/api/v1/push"
tenant_id = "prod"
basic_auth {
username = "ingest"
password_file = "/etc/alloy/secrets/loki-pass"
}
tls {
ca_file = "/etc/alloy/certs/ca.pem"
}
}
}
The redaction stages run before the regex and label stages. The order is the discipline: redact first, parse second, label third, write fourth.
How to validate it
Validation has three parts: configuration, certificate, and behaviour.
# CONFIGURATION: parse-check.
alloy validate /etc/alloy/config.alloy
ts=2026-08-13T13:11:04Z level=info msg="config valid"
# READ-ONLY: verify the certificate chain from collector to Loki.
openssl s_client -connect loki.internal.example.com:443 \
-CAfile /etc/alloy/certs/ca.pem \
-cert /etc/alloy/certs/client.pem \
-key /etc/alloy/certs/client-key.pem \
-verify_return_error -showcerts < /dev/null
Verify return code: 0 (ok)
# READ-ONLY: confirm the writer is shipping with TLS.
curl -s http://localhost:12345/metrics | grep loki_write_sent_entries_total
loki_write_sent_entries_total{component="loki.write.loki"} 1872
# READ-ONLY: confirm the redaction ran.
curl -s http://localhost:12345/metrics | grep loki_process_dropped
# (no output; the redaction rewrites, not drops)
# READ-ONLY: query Loki for the redacted marker to confirm
# the redaction is end-to-end.
logcli query '{job="checkout"} |= "REDACTED_CC"' --since=1h
{job="checkout",host="app-007.example.com"} request_body=[REDACTED_CC] status=200
If the response shows [REDACTED_CC] instead of the raw
card number, the redaction is end-to-end. If the raw number
appears, the redaction did not run, or it ran downstream of
the writer.
How it can fail
Five failure modes specific to collector security.
- The literal password in the committed config. A team
committed a Loki basic_auth password to the Alloy config.
Symptom: the password rotated every quarter because the
previous quarter’s was leaked in a pull request. The fix
is to replace the literal with
password_fileand mount the secret from a Kubernetes Secret. - The redaction that ran after the writer. A team added
a regex redaction to Loki’s
derived_fieldsconfig. The redaction ran query-side; the unmasked lines were already in the chunks. Symptom: the Grafana panels showed[REDACTED_CC]; the chunks in Loki’s storage showed the raw number. - The certificate that expired. A team deployed a Loki
certificate with a one-year validity. The certificate
expired; the collector’s TLS handshake failed; the agents
stopped shipping. Symptom:
loki_write_sent_entries_totalis flat;loki_write_failed_entries_totalclimbs; the agent log showsx509: certificate has expired or is not yet valid. - The mTLS that was TLS-only. A team configured TLS for
the gateway’s receiver but not for the agents. Symptom:
the agents shipped over plaintext within the cluster
network; an attacker on the cluster network could inject
fake log lines. The fix is
require_client_cert: trueon the receiver andclient_ca_fileon the agent’s exporter. - The audit trail that was the collector’s stdout. A
team relied on the collector’s log output as the audit
trail. The collector’s debug log included the password on
a connection failure. Symptom: the password was in the
agent’s log file; the log file was shipped to a public S3
bucket. The fix is to set
logging.format: logfmtand to not enable debug logging in production.
How to troubleshoot it
When the TLS handshake fails, the order matters.
- Read the error. The agent log shows the X.509 error.
The error names the failure:
certificate has expired,certificate signed by unknown authority,no client certificate presented. The first error is the one to fix. - Verify the chain with
openssl.openssl s_client -connect host:port -CAfile ca.pem -cert client.pem -key client-key.pemshows the chain and the verification result. A return code other than 0 means the chain does not validate. - Check the file permissions. The
loki-passfile must be readable by the collector user and not by other users. Achmod 0644file is readable by everyone; achmod 0400is readable only by the owner. - Check the secret rotation. A
Secretin Kubernetes rotates by creating a newSecretand reloading the pod. A staleSecretis the most common cause of authentication failures after a rotation. - Smoke test with a known line. Ship a line that contains a known card number; confirm the redaction marker appears in Loki. The smoke test should be automatic; manual smoke tests do not run.
Security implications
The collector’s security surface is summarised here.
- TLS to backends. Mandatory.
insecure_skip_verify: trueis a development-only flag. - mTLS between collectors. Mandatory in production.
require_client_cert: trueon the receiver. - Secrets. Never as literals. Always as
${env:VAR}or${file:/path}references. - Redaction. Source-side, upstream of the writer.
- Audit trail. The config diff in the platform repository; the timestamp on the collector image; the agent log filtered for config reload events.
Performance implications
- TLS handshake cost. The first connection to a backend
is the most expensive; subsequent connections reuse the
session. The
sending_queueon the exporter keeps the connection alive; the cost is amortised. - Redaction cost. A
stage.replaceruns every entry through a regex. A complex regex on every line is expensive; keep the regex specific to the pattern that needs redaction. - mTLS cost. The verification of the client certificate adds a small CPU cost per connection. The cost is negligible compared to the TLS handshake itself.
Production guidance
- Source-side redaction. Redact before the writer. The chunks in Loki’s storage must never contain the unmasked data.
- Secrets in files, never literals. Mount the secret
from a Kubernetes Secret with
chmod 0400permissions. - mTLS everywhere. TLS to the backends, mTLS between
collectors.
require_client_cert: trueon the receivers. - Audit trail. The config diff is the audit trail.
Commit every change; tag every change with the
last_verifieddate; back up the platform repository. - Rotate credentials on a schedule. A leaked credential is a credential that rotates. A scheduled rotation quarterly is the minimum; a rotation triggered by a suspected leak is the maximum.
Verification
You should now be able to answer:
- What are the three TLS configuration knobs on a Loki exporter, and what does each one do?
- Why must redaction run before the writer, not at query time?
- What is the difference between
${env:VAR}and${file:/path}for secrets management? - Why is
insecure_skip_verify: truea development-only flag?
Quiz
Knowledge check · 8 questions
Q1. In an OTel Collector exporter, the TLS knob that disables certificate verification is:
Q2. Redaction should run:
Q3. A literal password committed to the collector config is acceptable if the repository is private.
Q4. For mutual TLS between agents and a central gateway, the gateway receiver must set:
Q5. Name the OpenSSL command that verifies the certificate chain from the collector to the Loki backend.
Q6. Which of these are supported patterns for secrets management in the collector?
Q7. A team sets insecure_skip_verify: true because the CA bundle is missing. The most likely operational consequence is:
Q8. The audit trail for a collector configuration change is:
Passing score: 75%. Answers are checked in this browser.