Zum Inhalt springen
Open Source Apache-2.0 · cosign-signiert · SBOM für jedes Release

Zugriff definieren.
Verbindungen entstehen.

Verbinde Kubernetes-Workloads mit den Systemen, die sie brauchen — in deinem eigenen Headscale-Tailnet. 
Definiere den Zugriff in YAML; Tsunaro verwaltet die Verbindung und meldet die Erreichbarkeit.

Vom Workload zum entfernten System

Vom Cluster zu den Services an deinem Standort.

Deine Anwendung nutzt einen Service-Namen im Cluster. Tsunaro verwaltet den Egress-Agent, der sie über dein Tailnet mit dem entfernten Service verbindet.

  1. Dein ClusterKubernetes-WorkloadNamespace mit Zugriffsberechtigung
  2. Von Tsunaro verwaltetService + Egress-AgentMit TailnetEgress deklariert
  3. Dein StandortEntfernter ServiceÜber einen Subnet-Router erreichbar
Die verschlüsselte Tailnet-Verbindung besteht zwischen dem Egress-Agent und deinem Subnet-Router. Headscale koordiniert Identitäten und Policy, transportiert aber keinen Anwendungsverkehr.

Die Operator-Dokumentation und Codebeispiele sind auf Englisch.

Du benötigst einen eigenen Headscale-Server. Die Policy-Verwaltung ist optional; wer Custom Resources schreiben darf, muss vertrauenswürdig sein. Lies vor dem Deployment das Sicherheitsmodell (Englisch) .

Tsunaro im Überblick

Einmal deklarieren, der Operator übernimmt

Starte mit einem Manifest für dein Szenario. Tsunaro verwaltet Verbindungen oder Routen und meldet Status und Ursachen. Aktiviere die Policy-Verwaltung, um Tags, Freigaben und Grants erzeugen zu lassen — oder verwalte sie selbst.

5 Szenarien — wähle eines aus. Scrolle seitlich durch die Liste für weitere Szenarien.

Eine Datenbank am Standort erreichen

Ein Namespace benötigt Zugriff auf eine Oracle-Datenbank und einen Broker im LAN. Ein TailnetEgress benennt die Ziele; Anwendungen erreichen sie über Services im Cluster.

 1---
 2# One egress identity in the operator's namespace; consumerNamespaces
 3# decides who may reach its Services. The tag needs tagOwners, a /32
 4# route approval and a grant per target: with Tailnet.spec.policy the
 5# operator generates them, otherwise an admin writes them and the
 6# operator only reports what is missing. Each target becomes a Service
 7# named after it: <target>.<operator-namespace>:<port>.
 8apiVersion: tsunaro.io/v1alpha1
 9kind: TailnetEgress
10metadata:
11  name: billing-dev
12  namespace: tailnet-system
13spec:
14  tailnetRef:
15    name: corp
16  # tag: tag:ns-billing-dev      # default: tag:egress-{name}
17  # hostname: egress-billing-dev # default: egress-{name}
18  consumerNamespaces:
19    - billing-dev
20  targets:
21    - name: oracle-db-1
22      address: 10.0.10.20
23      port: 1521
24    - name: oracle-db-2
25      address: 10.0.10.21
26      port: 1521
27    - name: broker-1
28      address: 10.0.20.10
29      port: 5672
30    # A target routed by name (TLS SNI / HTTP Host) behind a reverse
31    # proxy at the far site: consumers must dial the real hostname, so
32    # publish it as an alias of the Service ClusterIP via
33    # Tailnet.spec.dns.hostsConfigMap. The L4 tunnel passes SNI through.
34    - name: api-corp
35      address: 10.0.30.10
36      port: 443
37      aliases:
38        - api.corp.example
Zum Guide (Englisch): Egress: reach services at the site

Einen Service im Tailnet bereitstellen

Ein Team stellt Postgres und Redis über einen Userspace-Node mit eigenem MagicDNS-Namen bereit. allowFrom bestimmt den Policy-Grant — also, wer die Verbindung aufbauen darf.

 1---
 2# One ingress identity in the tenant namespace: a single userspace node
 3# (MagicDNS name ingress-shop) exposing in-cluster Services on the
 4# tailnet. Each expose is a plain-TCP listener (L4, any protocol)
 5# forwarded to a Service in this namespace. Who may reach it is a
 6# policy grant with dst = the tag: with Tailnet.spec.policy the operator
 7# generates it from allowFrom (each entry must match the admin's
 8# ingressSourceAllowlist, else PolicyDenied); otherwise an admin writes
 9# tag and grant and the operator only reports what is missing.
10# A tailnet peer then dials e.g. ingress-shop:5432.
11apiVersion: tsunaro.io/v1alpha1
12kind: TailnetIngress
13metadata:
14  name: shop
15  namespace: shop-dev
16spec:
17  tailnetRef:
18    name: corp
19  # tag: tag:ingress-shop      # default: tag:ingress-{name}
20  # hostname: ingress-shop     # default: ingress-{name}
21  allowFrom: [group:shop]      # grant src (operator-owned policy only)
22  exposes:
23    - name: postgres
24      port: 5432               # port the tailnet node listens on
25      backend:
26        service: shop-db-rw    # Service in this namespace
27        port: 5432
28    - name: redis
29      port: 6379
30      backend:
31        service: shop-redis
32        port: 6379
33    # HTTPS funnel: TLS still terminates at the shared Envoy gateway
34    # (wildcard cert, HTTPRoute by Host), but the tailnet identity — and
35    # so the grant — is this app's own node. Needs
36    # Tailnet.spec.dns.ingressNameTemplate; status.dnsName resolves to
37    # this node and is the hostname the tenant puts on its HTTPRoute.
38    # - name: https
39    #   port: 443
40    #   backend:
41    #     service: envoy-prod-1-tailnet
42    #     namespace: envoy-gateway-system   # cross-namespace backend
43    #     port: 443
Zum Guide (Englisch): Ingress: publish a TCP service

Policy prüfen, dann anwenden

Die Basis-Policy liegt in Git. Der Operator ergänzt tagOwners, Routenfreigaben und Grants aus den Custom Resources. Starte mit DryRun, prüfe das Ergebnis und wechsle dann zu Apply.

 1---
 2# A control server the cluster talks to. Cluster-scoped: one per
 3# Headscale, shared by every CR. The bootstrap API key is read-only; the
 4# operator mints and rotates its own working key from it into
 5# <name>-apikey, which is not managed by GitOps.
 6apiVersion: tsunaro.io/v1alpha1
 7kind: Tailnet
 8metadata:
 9  name: corp
10spec:
11  server: https://ts.example.com
12  # In-cluster REST endpoint if it differs from the public login URL.
13  apiUrl: http://headscale.tailnet-system.svc:8080
14  apiSecretRef:
15    name: headscale-bootstrap
16    key: apiKey
17  # apiKeyLifetime: 720h   # working key, rotated at half-life
18  # authKeyLifetime: 168h  # per-CR pre-auth key, rotated at half-life
19  proxy:
20    # Optional registry Secret names; must exist in EVERY agent
21    # namespace. Omit to inherit AGENT_IMAGE_PULL_SECRETS from the
22    # operator; [] clears that default.
23    # imagePullSecrets: [dockerconfig]
24    mtu: 1200
25    probeInterval: 5m
26  # DNS names (optional). The operator renders a CoreDNS `hosts` file
27  # into this ConfigMap (operator namespace, key "hosts"): egress target
28  # aliases -> Service ClusterIP, ingress dnsNames -> node tailnet IPs.
29  # Mount it into a resolver authoritative for the zones
30  # (docs/how-to/dns-coredns.md). Keep the ingress name ONE label below
31  # the gateway's wildcard cert, e.g. *.prod-1.tailnet.example.internal.
32  dns:
33    hostsConfigMap: tailnet-dns-hosts
34    ingressNameTemplate: "{name}-{namespace}.prod-1.tailnet.example.internal"
35    # Optional short alias for HTTP exposes with `alias: true`. Must end
36    # in the SAME zone as ingressNameTemplate.
37    # ingressAliasTemplate: "{name}.prod-1.tailnet.example.internal"
38  # Operator-owned policy (optional; needs Headscale policy mode
39  # "database", docs/how-to/policy.md). The base document
40  # (policy-base.jsonc) lives in Git -> ConfigMap; the operator appends
41  # tagOwners, /32 route approvals and grants derived from the CRs and
42  # writes the union. Start in DryRun, read <baseConfigMap>-rendered,
43  # then switch to Apply.
44  policy:
45    baseConfigMap: headscale-policy
46    mode: DryRun
47    tagOwners: [group:operator]
48    routeApprovers: [tag:fabric]
49    ingressSourceAllowlist: ["group:*"]
50  # Address space no TailnetSite may claim (optional): the node prefix
51  # of this control server and, per cluster, LAN/pod/service/tailnet-LB
52  # ranges.
53  reservedNetworks:
54    - name: nodes
55      prefixes: [100.64.0.0/16]
56    - name: prod-1
57      prefixes: [192.0.2.0/24, 10.244.0.0/16, 10.96.0.0/12, 198.51.100.0/24]
Zum Guide (Englisch): Let the operator own the policy

Ein Standortnetz anbinden

Ein Subnet-Router kündigt ein /24-Standortnetz und einen Management-Host an. Der Operator genehmigt die Routen, wendet sie auf alle ankündigenden Router an und meldet Konflikte mit reservierten Adressbereichen.

 1---
 2# A remote network a tagged subnet router advertises into the tailnet:
 3# a branch (/24), a management range or a single host (/32).
 4# Cluster-scoped, in the cluster whose operator owns the policy (mode
 5# Apply). The operator appends autoApprovers for the prefixes, replays
 6# the approval on every node carrying an advertiser tag — in this or
 7# another cluster — and reports
 8# Advertised/Approved/Ready. Who may reach the prefixes stays a grant in
 9# the base policy. Overlap with Tailnet.spec.reservedNetworks or an
10# older TailnetSite blocks the CR (RouteConflict) until resolved.
11# docs/how-to/site-network.md: a branch behind a tagged router you run and
12# the prod-1 LAN behind the operator-managed gateway
13# (deploy/examples/tailnetgateway.yaml) are declared the same way.
14apiVersion: tsunaro.io/v1alpha1
15kind: TailnetSite
16metadata:
17  name: branch-north
18spec:
19  tailnetRef:
20    name: corp
21  prefixes:
22    - 203.0.113.0/24
23    - 198.18.0.17/32
24  # Routers allowed to advertise these prefixes. Omit to use
25  # Tailnet.spec.policy.routeApprovers (the gateway's tag).
26  advertisedBy:
27    tags: [tag:fabric]
28  owner: netops
29  description: Branch office north, reached through the on-prem subnet router
Zum Guide (Englisch): Declare a site network (TailnetSite)

Das Gateway als Custom Resource betreiben

Ein Subnet-Router für das Cluster-LAN und den Tailnet-Load-Balancer-Bereich, dazu eine Split-DNS-Zone — einmal deklariert und pausiert, bis die Voraussetzungen erfüllt sind.

 1---
 2# The gateway of docs/how-to/gateway.md: subnet router for the prod-1
 3# LAN and the tailnet LB range, plus the split-DNS zone served from the
 4# operator's hosts file. Shipped with suspend: true so the fixture
 5# admits without creating anything; set suspend: false once the
 6# prerequisites are in place (kernel, LB pool matching poolLabels,
 7# policy Apply, Headscale split-DNS). The zone is prod.example.internal
 8# here (the envtest DNS fixtures pin it); the how-to uses
 9# prod-1.tailnet.example.internal.
10apiVersion: tsunaro.io/v1alpha1
11kind: TailnetGateway
12metadata:
13  name: router
14spec:
15  suspend: true
16  deletionPolicy: Retain
17  tailnetRef:
18    name: corp
19  hostname: prod-router
20  tags: [tag:fabric]
21  stateSecretRef:
22    name: router-state
23  # Version tag or digest; prefer a digest in production.
24  image: ghcr.io/tailscale/tailscale:v1.102.4
25  routes:
26    lan: [192.0.2.0/24]
27    advertiseEgressTargets: true
28  service:
29    loadBalancerIP: 198.51.100.17
30    tailnetLBCIDR: 198.51.100.0/24
31    poolLabels:
32      io.cilium/lb-pool: tailnet
33  dns:
34    zone: prod.example.internal
35    serviceName: router-dns
36    serviceClusterIP: 10.96.0.54
37    image: docker.io/coredns/coredns:1.14.7
38    staticRecords:
39      - name: api
40        target: kube-api-platform.prod.example.internal
41      - name: router
42        address: 192.0.2.1
43    wildcardAddress: 198.51.100.80
Zum Guide (Englisch): Run the gateway (subnet router + split-DNS)

Mit einem festen Release starten

Diese Befehle installieren den Operator und warten auf sein Deployment. Sie konfigurieren weder Headscale noch gewähren sie Netzwerkzugriff.

  • Kubernetes ab Version 1.31, ein CNI mit Durchsetzung von NetworkPolicy, Helm und kubectl.
  • Ein erreichbarer Headscale-Server der Version 0.29.x. Das Tutorial benötigt außerdem einen Subnet-Router, eine erreichbare Datenbank am Standort und ein registriertes Client-Gerät.

Noch kein Control-Server? Headscale vorbereiten . Vor dem Deployment: Release-Signaturen prüfen . Beide Anleitungen sind auf Englisch.

Operator installieren · v0.18.0
helm install tailnet-operator oci://oci.this-is-fine.io/charts/tailnet-operator \
  --version 0.18.0 \
  --namespace tailnet-system --create-namespace

kubectl --namespace tailnet-system rollout status deployment/tailnet-operator

Als Nächstes: Zugriff konfigurieren. Das Schritt-für-Schritt-Tutorial (Englisch) beschreibt den Bootstrap-API-Key, sein Kubernetes Secret, Policy-Einträge und vollständige Tailnet-, Egress- und Ingress-Manifeste. Ersetze die Beispieladressen und Namen durch deine eigenen.

Für den produktiven Einsatz: Installation und Voraussetzungen (Englisch) . Die Beispiele und Befehle auf dieser Website beziehen sich auf v0.18.0.

SBOM-Downloads · v0.18.0

Software-Stücklisten (SBOMs) erfassen die Software im jeweiligen Plattform-Image. Sie sind weder Schwachstellenberichte noch Compliance-Zertifikate.

linux/amd64

SPDX-2.3 · 253.8 KiB

SPDX-JSON herunterladen Signiertes Sigstore-Bundle · 270.2 KiB
Offline prüfen und extrahieren

Lade das Bundle und den öffentlichen Schlüssel in dasselbe Verzeichnis herunter. Mit installiertem Cosign 3, jq und base64 prüfen diese Befehle die signierte eingebettete SBOM, nicht die separat heruntergeladene JSON-Datei. Nach erfolgreicher Prüfung extrahieren sie die authentifizierte SBOM in eine Datei mit dem Präfix verified-.

cosign verify-blob-attestation \
  --key cosign.pub \
  --insecure-ignore-tlog \
  --type spdxjson \
  --digestAlg sha256 \
  --digest 46ee66545446b4886498f625a931fc9f4a0dac6ecb61f6caed76bd0a438353d4 \
  --bundle linux-amd64.sigstore.json &&
# Extract the authenticated SBOM only after verification succeeds.
jq -r '.dsseEnvelope.payload' linux-amd64.sigstore.json \
  | base64 --decode \
  | jq '.predicate' > verified-linux-amd64.spdx.json

linux/arm64

SPDX-2.3 · 253.8 KiB

SPDX-JSON herunterladen Signiertes Sigstore-Bundle · 270.2 KiB
Offline prüfen und extrahieren

Lade das Bundle und den öffentlichen Schlüssel in dasselbe Verzeichnis herunter. Mit installiertem Cosign 3, jq und base64 prüfen diese Befehle die signierte eingebettete SBOM, nicht die separat heruntergeladene JSON-Datei. Nach erfolgreicher Prüfung extrahieren sie die authentifizierte SBOM in eine Datei mit dem Präfix verified-.

cosign verify-blob-attestation \
  --key cosign.pub \
  --insecure-ignore-tlog \
  --type spdxjson \
  --digestAlg sha256 \
  --digest ffd8df293413ea62fae76a7b3ea03fe188aee3f618278e2f5eb8b2e4e1c55de0 \
  --bundle linux-arm64.sigstore.json &&
# Extract the authenticated SBOM only after verification succeeds.
jq -r '.dsseEnvelope.payload' linux-arm64.sigstore.json \
  | base64 --decode \
  | jq '.predicate' > verified-linux-arm64.spdx.json
Was die Prüfung belegt

Gleiche cosign.pub vor der Prüfung über einen vertrauenswürdigen Kanal mit dem Schlüssel im Release-Quellcode ab. Ein neben dem Bundle heruntergeladener Schlüssel ist kein unabhängiger Vertrauensanker.

Prüfsummen erkennen Beschädigungen, belegen aber keine Authentizität. Die Offline-Prüfung kontrolliert die Signatur und den Digest des Plattform-Images, nicht die Aufnahme in ein Transparenzprotokoll.

Dokumentation entdecken

Die Operator-Dokumentation und Codebeispiele sind auf Englisch.