Security¶
This page describes the operational security posture of tailscale2otel: what data it
handles, where that data goes, and the configuration levers and footguns operators should
be aware of.
Telemetry payload sensitivity¶
tailscale2otel exports network metadata about your tailnet. Flow logs and audit logs
carry, among other things:
- source and destination IP addresses and ports,
- device names and hostnames,
- user identities (e.g. the actor on an audit event).
All of this is exported over OTLP to the configured backend (for example Grafana Cloud). Treat the OTLP backend as a trusted data sink — anyone with read access to it can see this metadata. Scope backend credentials accordingly.
Levers to reduce what leaves the tailnet (all under the cardinality: block in
config.example.yaml):
cardinality.flow.source_port/cardinality.flow.destination_port(both defaultfalse) — keep ports off flow metrics. Ports are always present on flow logs regardless of these settings.cardinality.flow.collapse_external(defaulttrue) — buckets unresolved IPs asexternal/unknownrather than emitting them as distinct series/labels.cardinality.flow.node_dims(defaulttrue) — setfalseto omit src/dst device names from flow metrics.
Disabling devices does not remove IPs
Disabling the devices collector does not remove IP addresses from the exported
payload — it only degrades IP→name enrichment, so flow/audit records fall back to
unknown/external for names while the raw addresses are still exported.
Traces export a third, unaggregated stream
When tracing.enabled: true, spans are also exported to the OTLP backend. Unlike metrics,
spans are unaggregated: useful identifiers such as the tailnet name and device IDs appear
on the url.full attribute of tailscale.api spans by design. Auth headers, tokens, and
response bodies are never attached, and per-record source/destination IPs are not placed on
receiver spans. Account for this extra stream when scoping data residency or export policies;
tracing is off by default.
ACL policy hygiene¶
The acl collector scores the tailnet policy for structural risk on each tick (no extra API
call — it reuses the policy it already fetches). Only bounded structural counts with enum labels
are emitted; rule contents and PII are never exported. The risk gauges are:
tailscale.acl.wildcard_rules— rules with a*source or destination (by section/position).tailscale.acl.unrestricted_rules— any-to-any non-deny rules (> 0means at least one rule matches any source to any destination).tailscale.acl.autoapprovers— auto-approver depth by kind (routes/exit_node/services);autoapprover_kind="exit_node" > 0means exit-node auto-approval is configured.tailscale.acl.ssh_wildcard— Tailscale SSH rules with a wildcard source or destination.tailscale.acl.posture_gated_rules— rules gated bysrcPosture.
These make natural alert conditions — see Alerts.
Audit change tracking¶
In addition to the raw tailscale.config.audit.events counter (action + origin), a curated
tailscale.config.audit.changes counter emits only security- and lifecycle-relevant events,
categorized by the tailscale.audit.change attribute (Prometheus label tailscale_audit_change;
values include acl, key_expiry, exit_node, tailnet_lock, user_role, auth_provider,
secret, device, api_key, posture_integration, magic_dns, dns_config, …) and actor.type.
It is purpose-built for alerting on high-value change categories (and device churn) without the
noise of the full audit stream.
Outbound version checks¶
By default tailscale2otel makes two unauthenticated outbound HTTPS GET requests to power its
update-available and version-skew signals:
version_checks.self(defaulttrue) →api.github.com/repos/rknightion/tailscale2otel/releases/latestversion_checks.devices(defaulttrue) →pkgs.tailscale.com/stable/?mode=json
No tailnet data is sent in either request. Set both to false in air-gapped or egress-controlled
environments.
Receiver authentication footguns¶
The optional streaming (Splunk-HEC) and webhook receivers accept inbound POSTs. Their
authentication is opt-in by presence of a secret, so an empty value silently disables
it (these behaviours are also noted in
config.example.yaml):
- Leaving
webhook.secretempty skips HMAC verification entirely — unauthenticated POSTs are accepted. - Leaving
streaming.tokenempty disables receiver authentication.
Always set credentials before exposing a receiver
Always set these when exposing a receiver, especially on a wildcard/all-interfaces bind
or without TLS. Tailscale requires HTTPS for the streaming sink; a tailscale cert
works for private tailnet endpoints.
Mistyped environment variable names silently leave auth disabled
Any field can be set via a TS2OTEL_* environment variable (the env layer overrides the
file), and an empty credential silently disables auth — for example a mistyped
variable name (TS2OTEL_WEBHOOK__SECRT) leaves the secret empty rather than failing
loudly. The startup log WARNs on a TS2OTEL_* variable that matches no config key, but
double-check that auth credentials are actually set.
streaming.auto_configure footgun¶
auto_configure overwrites your existing log-streaming sink
When streaming.auto_configure: true, the service registers this receiver as the
tailnet's log-streaming sink on startup, and overwrites any existing sink configured
for the tailnet. Never enable it against a tailnet whose streaming configuration you do
not intend to replace. It is off by default.
Secrets handling¶
- Keep secrets in
TS2OTEL_*environment variables (the env layer overrides the file), never as literal values in YAML.config.local.yaml,.env.local, and.secrets/are gitignored for this reason. - The admin status page redacts secret values — it emits only
*Setbooleans (e.g.webhook_secret_set) and OTLP header key names, never the values themselves. - Secrets are never logged.
pprof requires admin auth
Enabling profiling.pprof.enabled requires both admin.enabled and
admin.auth.token, because heap and goroutine dumps can expose in-memory secrets.
See configuration.md for the full config reference and
env-vars.md for the complete list of TS2OTEL_* environment variables.