Skip to content
This repository was archived by the owner on May 31, 2026. It is now read-only.

home-operations/pangolin-operator

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

133 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

pangolin-operator

Project archived, check out https://github.com/fosrl/pangolin-kube-controller

A Kubernetes operator that manages Pangolin tunnel infrastructure through native Kubernetes resources. It provisions Pangolin sites, manages the newt tunnel Deployment, and continuously reconciles public and private resources against the Pangolin API.

How it works

┌────────────────────────────────────────────────────┐
│  Kubernetes cluster                                │
│                                                    │
│  NewtSite CR ──► creates Pangolin site             │
│               ──► deploys newt Deployment          │
│               ──► watches HTTPRoutes / Services    │
│                                                    │
│  PublicResource CR ──► Pangolin public resource    │
│                         (HTTP / TCP / UDP)         │
│                                                    │
│  PrivateResource CR ──► Pangolin site resource     │
│                          (OLM VPN: host/cidr/port) │
└────────────────────────────────────────────────────┘

The operator calls the Pangolin REST API directly. No blueprint files, no sidecars.

CRDs

Kind Short name Description
NewtSite nsite Pangolin site + newt tunnel Deployment
PublicResource pubr Pangolin public resource (HTTP, TCP, UDP)
PrivateResource privr Pangolin private/OLM resource

All CRDs are namespaced and live under pangolin.home-operations.com/v1alpha1.

Prerequisites

Enable the Pangolin Integration API

This operator communicates exclusively with the Pangolin Integration API. The Integration API is disabled by default in Pangolin — you must enable it before deploying the operator.

In your Pangolin config.yml:

flags:
    enable_integration_api: true

The API listens on port 3003 by default. Expose it via Traefik (or another reverse proxy) so the operator can reach it. The PANGOLIN_API_URL environment variable should point to the exposed base URL (e.g. https://api.example.com).

See the Pangolin Integration API docs for the full setup including Traefik routing configuration and Swagger UI access.

You will need to create an Org API key with at least the following permissions:

  • List Organization Domains
  • All for Sites
  • All for Resources
  • All for Targets
  • All for Resource Rules

Configuration

The operator reads its Pangolin credentials from environment variables:

Variable Description
PANGOLIN_API_URL Pangolin API base URL (e.g. https://pangolin.example.com)
PANGOLIN_API_KEY Pangolin API key
PANGOLIN_ORG_ID Pangolin organisation ID
PANGOLIN_ENDPOINT Endpoint passed to newt pods (PANGOLIN_ENDPOINT env var)

NewtSite

A NewtSite provisions a Pangolin site and — unless type: local — manages a Deployment running the newt tunnel daemon.

apiVersion: pangolin.home-operations.com/v1alpha1
kind: NewtSite
metadata:
    name: homelab
    namespace: network
spec:
    name: Homelab
    type: newt # "newt" (default) or "local"
    newt:
        image: ghcr.io/fosrl/newt
        tag: latest
        replicas: 1
        logLevel: INFO # DEBUG | INFO | WARN | ERROR
        mtu: 1380 # WireGuard MTU (default: 1280)
        pingInterval: "60s" # WireGuard keepalive interval (PING_INTERVAL env)
        pingTimeout: "5s" # WireGuard ping timeout (PING_TIMEOUT env)
        interface: "newt" # WireGuard interface name (INTERFACE env, default "newt")
        dns: "1.1.1.1" # custom DNS pushed into tunnel (DNS env)
        acceptClients: false # accept incoming VPN clients (ACCEPT_CLIENTS env)
        resources:
            requests:
                cpu: 10m
                memory: 32Mi
    autoDiscover:
        annotationPrefix: pangolin-operator # default
        enableRouteDiscovery: false # enable HTTPRoute auto-discovery (default: false)
        enableServiceDiscovery: false # enable Service auto-discovery (default: false)
        gatewayName: envoy-gateway # filter HTTPRoutes by parentRef gateway
        gatewayNamespace: network
        gatewayTargetHostname: envoy-external.network.svc.cluster.local # override target hostname for gateway-based discovery
        ssl: true # default SSL for HTTP resources
        denyCountries: "RU,CN,KP,IR"

The operator auto-creates a Secret named <site>-newt-credentials containing PANGOLIN_ENDPOINT, NEWT_ID, and NEWT_SECRET. The newt Deployment reads credentials from this Secret.

WireGuard native interface

Set newt.useNativeInterface: true to use the kernel WireGuard module instead of the userspace implementation. This runs the pod as root with NET_ADMIN and SYS_MODULE capabilities. Only use this when the node kernel has the WireGuard module loaded.

spec:
    newt:
        useNativeInterface: true
        hostNetwork: true # optional: grant host network namespace
        hostPID: false

Scheduling and placement

spec:
    newt:
        nodeSelector:
            kubernetes.io/arch: amd64
        tolerations:
            - key: node-role.kubernetes.io/control-plane
              operator: Exists
              effect: NoSchedule
        affinity:
            nodeAffinity:
                requiredDuringSchedulingIgnoredDuringExecution:
                    nodeSelectorTerms:
                        - matchExpressions:
                              - key: topology.kubernetes.io/zone
                                operator: In
                                values: [us-east-1a]
        topologySpreadConstraints:
            - maxSkew: 1
              topologyKey: kubernetes.io/hostname
              whenUnsatisfiable: DoNotSchedule
              labelSelector:
                  matchLabels:
                      app.kubernetes.io/instance: homelab

Metrics

Set newt.metrics to expose a Prometheus metrics endpoint. The operator sets NEWT_ADMIN_ADDR to bind the metrics server inside the container and adds a named metrics container port.

spec:
    newt:
        metrics:
            port: 2112 # metrics container port (default 9090)
            # adminAddr: "0.0.0.0:2112"  # override full bind address if needed

Extra environment variables, volumes, and containers

spec:
    newt:
        extraEnv:
            - name: BLUEPRINT_FILE
              value: /config/blueprint.json
            - name: NEWT_METRICS_PROMETHEUS_ENABLED
              value: "true"
        extraVolumes:
            - name: blueprint-config
              configMap:
                  name: my-blueprint
        extraVolumeMounts:
            - name: blueprint-config
              mountPath: /config
              readOnly: true
        initContainers:
            - name: wait-for-dependency
              image: busybox:latest
              command: ["sh", "-c", "until wget -qO- http://dep/health; do sleep 2; done"]
        extraContainers:
            - name: sidecar
              image: my-sidecar:latest

Security context overrides

By default the operator enforces a secure non-root context (or root+privileged for native WireGuard). Both contexts can be replaced entirely:

spec:
    newt:
        podSecurityContext:
            runAsNonRoot: true
            seccompProfile:
                type: RuntimeDefault
        securityContext:
            runAsUser: 65534
            allowPrivilegeEscalation: false
            capabilities:
                drop: [ALL]

PublicResource

Manages a Pangolin public resource. The siteRef field references a NewtSite in the same (or another) namespace.

HTTP

apiVersion: pangolin.home-operations.com/v1alpha1
kind: PublicResource
metadata:
    name: my-app
    namespace: default
spec:
    siteRef: homelab
    name: My App
    protocol: http
    fullDomain: app.example.com
    ssl: true
    targets:
        - hostname: my-app.default.svc.cluster.local
          port: 8080
          method: http # http | https | h2c

Wildcard subdomains (Pangolin 1.18+)

Set fullDomain to a wildcard like *.apps.example.com to route every hostname at that level through the same resource. The original Host header is preserved so downstream routing keeps working.

spec:
    fullDomain: "*.apps.example.com"

Wildcards require a TLS certificate that covers *.apps.example.com, which means DNS-01 validation in your Pangolin/Traefik setup. HTTP-01 only proves a single exact hostname.

TCP / UDP

spec:
    siteRef: homelab
    name: Forgejo SSH
    protocol: tcp # tcp | udp
    proxyPort: 2222
    targets:
        - hostname: forgejo.selfhosted.svc.cluster.local
          port: 22

Auth

spec:
    auth:
        ssoEnabled: true
        ssoRoles:
            - Member
        autoLoginIdp: 1
        authSecretRef: myapp-auth # Kubernetes Secret name

Secret keyspincode, password, basic-auth-user, basic-auth-password.

Access control rules

spec:
    rules:
        - action: DROP
          match: country
          value: RU
        - action: ACCEPT
          match: cidr
          value: 10.0.0.0/8
          priority: 10

Valid action values: ACCEPT, DROP, PASS. Valid match values: ip, cidr, path, country.

Cross-namespace site reference

The operator resolves NewtSite by name across all namespaces using a field index, so only the site name is needed — no namespace field required.

spec:
    siteRef: homelab

PrivateResource

Registers a host, CIDR range, or HTTP web app with the Pangolin OLM VPN. Clients with the appropriate roles gain access through the newt tunnel.

host / cidr

apiVersion: pangolin.home-operations.com/v1alpha1
kind: PrivateResource
metadata:
    name: cluster-pods
    namespace: network
spec:
    siteRef: homelab
    name: Cluster Pod Network
    mode: cidr # host | cidr | http
    destination: 10.42.0.0/16
    tcpPorts: "*"
    udpPorts: "*"
    disableIcmp: false
    roleIds: [1, 2]
    userIds: []
    clientIds: []

In host mode, destination can be an IP address or a hostname. If it is a hostname, alias (a FQDN) is required.

http (Pangolin 1.18+)

mode: http exposes a private web resource on a Pangolin-managed domain with a valid TLS certificate. The URL is only reachable when the user has an active Pangolin client connection — nothing is exposed on the public internet.

apiVersion: pangolin.home-operations.com/v1alpha1
kind: PrivateResource
metadata:
    name: internal-grafana
    namespace: monitoring
spec:
    siteRef: homelab
    name: Internal Grafana
    mode: http
    fullDomain: grafana.internal.example.com # must be a subdomain of a Pangolin-managed domain
    destination: grafana.monitoring.svc.cluster.local
    destinationPort: 3000
    scheme: http # http | https (backend protocol; default http)
    ssl: true # public-facing TLS (default true)
    roleIds: [1]

The fullDomain is resolved against the organisation's Pangolin domains the same way PublicResource.fullDomain is. Adoption of an existing Pangolin private HTTP resource matches by name + mode + fullDomain (rather than destination), so changing the backend hostname or port reuses the existing resource.

Auto-discovery

When autoDiscover is set on a NewtSite, the operator can watch HTTPRoute and Service resources and automatically create PublicResource CRs owned by the NewtSite. Both discovery modes are disabled by default and must be explicitly enabled.

Field Default Description
enableRouteDiscovery false Enable HTTPRoute auto-discovery
enableServiceDiscovery false Enable Service auto-discovery

HTTPRoute

HTTPRoute discovery is enabled by setting enableRouteDiscovery: true on autoDiscover. The operator processes every HTTPRoute hostname as a separate PublicResource. The backend target is derived from the first backendRef in the first rule.

apiVersion: gateway.networking.k8s.io/v1
kind: HTTPRoute
metadata:
    name: my-app
    namespace: default
    annotations:
        pangolin-operator/site-ref: homelab
spec:
    hostnames:
        - app.example.com
    rules:
        - backendRefs:
              - name: my-app
                port: 8080

HTTPRoute annotations

Annotation Description
pangolin-operator/site-ref Name of the NewtSite to use (required unless matched by gateway)
pangolin-operator/site-namespace Namespace of the NewtSite (defaults to route namespace)
pangolin-operator/enabled: "false" Opt out — skip this route
pangolin-operator/name Override resource display name (defaults to route name)
pangolin-operator/ssl: "false" Disable SSL
pangolin-operator/method Internal backend protocol: http, https, or h2c (default http)
pangolin-operator/host-header Override the Host header sent to the backend
pangolin-operator/tls-server-name Override the TLS SNI name (defaults to the hostname)
pangolin-operator/headers JSON array of extra headers: [{"name":"X-Foo","value":"bar"}]
pangolin-operator/auth-sso: "true" Enable SSO authentication
pangolin-operator/auth-sso-roles Comma-separated Pangolin roles (overrides site default)
pangolin-operator/auth-sso-users Comma-separated user e-mails (overrides site default)
pangolin-operator/auth-sso-idp Pangolin IdP ID for auto-login (overrides site default)
pangolin-operator/auth-whitelist-users Comma-separated user e-mails for whitelist
pangolin-operator/auth-secret Kubernetes Secret name containing sensitive auth values
pangolin-operator/maintenance-enabled: "true" Enable maintenance page
pangolin-operator/maintenance-type forced or automatic
pangolin-operator/maintenance-title Maintenance page title
pangolin-operator/maintenance-message Maintenance page body
pangolin-operator/maintenance-estimated-time Estimated duration
pangolin-operator/rules JSON array of access control rules
pangolin-operator/target-path Target path prefix, exact path, or regex
pangolin-operator/target-path-match prefix, exact, or regex
pangolin-operator/target-rewrite-path Rewrite request path to this value
pangolin-operator/target-rewrite-match exact, prefix, regex, or stripPrefix
pangolin-operator/target-priority Load-balancing priority (1–1000)
pangolin-operator/target-enabled "true" or "false" to enable/disable the target

Service

Services can be exposed in TCP/UDP mode or HTTP mode (when pangolin-operator/full-domain is set).

Service discovery is enabled by setting enableServiceDiscovery: true on autoDiscover. Once enabled, any Service annotated with pangolin-operator/site-ref is discovered and exposed through Pangolin. Annotate with pangolin-operator/enabled: "false" to opt a specific Service out.

Service annotations

Annotation Description
pangolin-operator/site-ref Name of the NewtSite (required)
pangolin-operator/site-namespace Namespace of the NewtSite
pangolin-operator/enabled "false" to opt out
pangolin-operator/full-domain Public domain — activates HTTP mode
pangolin-operator/port Port number or name to expose (required when Service has multiple ports and none named http)
pangolin-operator/protocol tcp or udp (TCP/UDP mode only)
pangolin-operator/all-ports: "true" Expose every Service port as a separate resource
pangolin-operator/name Override resource display name
pangolin-operator/method HTTP mode: http, https, or h2c
pangolin-operator/ssl HTTP mode: enable/disable SSL
pangolin-operator/host-header HTTP mode: override Host header
pangolin-operator/tls-server-name HTTP mode: override TLS SNI
pangolin-operator/headers HTTP mode: JSON array of extra headers
pangolin-operator/auth-sso HTTP mode: enable SSO
pangolin-operator/auth-sso-roles HTTP mode: SSO roles
pangolin-operator/auth-sso-users HTTP mode: SSO users
pangolin-operator/auth-sso-idp HTTP mode: auto-login IdP ID
pangolin-operator/auth-whitelist-users HTTP mode: whitelist users
pangolin-operator/auth-secret HTTP mode: Secret name for sensitive auth
pangolin-operator/maintenance-enabled HTTP mode: enable maintenance page
pangolin-operator/rules HTTP mode: JSON access control rules

Port selection (single-port mode)

When pangolin-operator/port is not set, the operator selects a port automatically:

  1. Service has exactly one port → use it
  2. Service has a port named http → use it
  3. Otherwise the Service is skipped

Gateway-based discovery

Instead of annotating every HTTPRoute with site-ref, set gatewayName on the NewtSite. The operator will process every HTTPRoute whose spec.parentRefs references that gateway, using the NewtSite name as the implicit site reference.

spec:
    autoDiscover:
        gatewayName: envoy-gateway
        gatewayNamespace: network

Individual routes can still override with pangolin-operator/site-ref or opt out with pangolin-operator/enabled: "false".

Custom annotation prefix

To avoid conflicts when running multiple operators or sites, set annotationPrefix on the NewtSite:

spec:
    autoDiscover:
        annotationPrefix: myorg

Then annotate resources with myorg/site-ref, myorg/enabled, etc.

Deployment

Install via Helm

helm install pangolin-operator oci://ghcr.io/home-operations/charts/pangolin-operator \
  --namespace pangolin-operator --create-namespace \
  --set pangolin.apiUrl=https://pangolin.example.com \
  --set pangolin.apiKey=<key> \
  --set pangolin.orgId=<org-id> \
  --set pangolin.endpoint=https://pangolin.example.com

Flux CD example

apiVersion: source.toolkit.fluxcd.io/v1
kind: OCIRepository
metadata:
    name: pangolin-operator
    namespace: flux-system
spec:
    interval: 15m
    ref:
        tag: 0.1.0
    url: oci://ghcr.io/home-operations/charts/pangolin-operator
---
apiVersion: helm.toolkit.fluxcd.io/v2
kind: HelmRelease
metadata:
    name: pangolin-operator
    namespace: pangolin-operator
spec:
    chartRef:
        kind: OCIRepository
        name: pangolin-operator
        namespace: flux-system
    interval: 1h
    values:
        pangolin:
            apiUrl: https://pangolin.example.com
            endpoint: https://pangolin.example.com
            orgId: <org-id>
            existingSecret: pangolin-operator-credentials # keys: PANGOLIN_API_URL, PANGOLIN_API_KEY, PANGOLIN_ORG_ID, PANGOLIN_ENDPOINT

Quick start

  1. Deploy the operator (see above).
  2. Create a NewtSite — the operator provisions the Pangolin site and deploys newt.
  3. Annotate HTTPRoute or Service resources, or create PublicResource / PrivateResource CRs directly.
# 1. Site
apiVersion: pangolin.home-operations.com/v1alpha1
kind: NewtSite
metadata:
    name: homelab
    namespace: network
spec:
    name: Homelab
    autoDiscover:
        enableRouteDiscovery: true
        gatewayName: envoy-gateway
        ssl: true
        denyCountries: "RU,CN,KP,IR"
---
# 2. Static public resource (no HTTPRoute needed)
apiVersion: pangolin.home-operations.com/v1alpha1
kind: PublicResource
metadata:
    name: forgejo-ssh
    namespace: network
spec:
    siteRef: homelab
    name: Forgejo SSH
    protocol: tcp
    proxyPort: 2222
    targets:
        - hostname: forgejo.selfhosted.svc.cluster.local
          port: 22
---
# 3. Private OLM resource
apiVersion: pangolin.home-operations.com/v1alpha1
kind: PrivateResource
metadata:
    name: cluster-pods
    namespace: network
spec:
    siteRef: homelab
    name: Cluster Pod Network
    mode: cidr
    destination: 10.42.0.0/16

About

No description, website, or topics provided.

Resources

License

Code of conduct

Contributing

Security policy

Stars

Watchers

Forks

Packages

 
 
 

Contributors