frp-operator

Expose your service in Kubernetes to the Internet with open source FRP!

Version: 1.11.0 Type: application AppVersion: 0.13.0 made with Go Github main branch build GitHub issues GitHub pull requestsArtifact Hub

Features

Custom Resources

Upstream protocols

Secure access to the FRP Server

Advanced traffic features

Operational features

Document

  1. RFC: Fast Reverse Proxy Operator

Installing

To install the chart with the release name my-release:

helm repo add frp-operator https://zufardhiyaulhaq.com/frp-operator/charts/releases/
helm install my-frp-operator frp-operator/frp-operator --values values.yaml

Prerequisite

To expose your private Kubernetes service into public network. You need public machine running FRP Server that act as a proxy. Currently the operator doesn’t have capability to spine a new machine on cloud providers, but this can be setup in a minute.

  1. Create machine on cloud provider
  2. Download frps binary
  3. Create server configuration ``` vi frps.ini

[common] bind_address = 0.0.0.0 bind_port = 7000 token = yourtoken

4. Run FRP server

frps -c ./frps.ini


You can reuse our build-in ansible playbook to setup the FRP server on your machine, please check https://github.com/zufardhiyaulhaq/frp-operator/tree/main/ansible/server

## Usage
1. Apply some example
```console
kubectl apply -f examples/deployment/
kubectl apply -f examples/client/
  1. Check frpc object ```console kubectl get client NAME AGE client-01 17m

kubectl get upstream NAME AGE nginx 17m


3. access the URL
```console
http://178.128.100.87:8080/

LoadBalancer Controller

Besides the hand-written Client/Upstream flow, the operator can act as a load-balancer controller: a Service of type: LoadBalancer with loadBalancerClass: frp.zufardhiyaulhaq.com/frp is bound to one frps server from a ServerPool, the operator generates the Client/Upstream resources in its own namespace, and the server’s public address lands in status.loadBalancer.ingress — exactly like a cloud load balancer, but through FRP. Charts and tools that already create LoadBalancer Services work unchanged.

ServerPool

A ServerPool lives in the operator namespace (next to the frps token Secrets) and lists the frps servers a Service may be bound to:

apiVersion: frp.zufardhiyaulhaq.com/v1alpha1
kind: ServerPool
metadata:
  name: prod
  namespace: frp-operator           # the operator's namespace
spec:
  allocationPolicy: Auto            # Auto (default): eligible without annotation | Explicit: only when a Service names it
  clientMode: PerService            # PerService (default): one frpc pod per Service per server | Shared: one frpc pod per server
  allowedPorts: ["80", "443", "8000-9000"]   # optional; empty = any port
  servers:
  - name: sg-01                     # stable key used in annotations and generated names (lowercase, max 40 chars)
    host: 178.128.100.87            # address frpc dials
    port: 7000
    publicAddress: 178.128.100.87   # IP or hostname written to the Service status
    allowedPorts: ["443"]           # optional per-server override
    authentication:
      token:
        secret:
          name: sg-01-token
          key: token
    # transportProtocol, tls, transport, adminServer: same shapes as Client.spec.server
  clientTemplate:                   # optional; applied to every generated Client
    podTemplate:
      resources:
        limits:
          memory: 64Mi

status.servers[].allocatedPorts shows which Service holds which port on each server, and the Ready condition reports validation problems (missing token Secret, duplicate or invalid server names, bad allowedPorts).

Service

apiVersion: v1
kind: Service
metadata:
  name: web
  namespace: default
  annotations:
    frp.zufardhiyaulhaq.com/server-pool: prod   # optional
    frp.zufardhiyaulhaq.com/server: sg-01       # optional; "sg-01,sg-02" for one IP per server
spec:
  type: LoadBalancer
  loadBalancerClass: frp.zufardhiyaulhaq.com/frp
  selector:
    app: web
  ports:
  - name: https
    port: 443          # opened as remotePort 443 on the frps server
    targetPort: 8443
    protocol: TCP

kubectl get svc web shows <pending> until the generated frpc client is Ready and its proxies are running, then the server’s publicAddress. kubectl describe svc web shows the Events explaining a pending state; the generated objects are lb-default-web-sg-01 (Client) and lb-default-web-sg-01-443 (Upstream) in the operator namespace.

Annotations

Annotation on the Service Meaning
frp.zufardhiyaulhaq.com/server-pool: <name> Use only this pool (required for pools with allocationPolicy: Explicit)
frp.zufardhiyaulhaq.com/server: sg-01[,sg-02] Bind to exactly these servers; one ingress IP each, all-or-nothing
frp.zufardhiyaulhaq.com/allocated-server Written by the operator; the current binding

Rules

Uninstalling / disabling

Every claimed Service carries the finalizer frp.zufardhiyaulhaq.com/loadbalancer, which the operator removes once it has cleaned up that Service’s generated Client/Upstream objects. Before running helm uninstall or setting loadBalancer.enabled=false, either delete the claimed Services or remove their loadBalancerClass so the operator can clean up first — otherwise those Services are left stuck deleting (or stuck with a stale finalizer) once the controller stops running.

If the operator is already gone and a Service is stuck on the finalizer, strip it manually:

kubectl patch svc <name> -p '{"metadata":{"finalizers":null}}' --type=merge

then delete the generated Client/Upstream objects yourself (label frp.zufardhiyaulhaq.com/managed-by=loadbalancer) in the operator namespace.

Security note

The operator matches Upstream objects to Client objects by name, across namespaces — an Upstream naming a generated Client (pool-<pool>-<server> in clientMode: Shared, lb-<ns>-<svc>-<server> in PerService) attaches to that Client’s live tunnel regardless of which namespace the Upstream lives in. This means any user who can create Upstream objects can attach to a generated Client if they can guess or discover its name. Restrict create on upstreams.frp.zufardhiyaulhaq.com to trusted namespaces via RBAC.

See examples/loadbalancer.

Server (frps)

Alpha. Server is new in v0.13.0. Its API (frp.zufardhiyaulhaq.com/v1alpha1, kind Server) and behaviour may change in a later release without a migration path; try it before relying on it in production.

The operator can also run frps itself. A Server becomes one frps pod behind a public Service (LoadBalancer by default), and private clusters connect ordinary Clients or ServerPool servers to it. The Service exposes the bind ports, the vhost ports and every port in allowPorts, each with its own protocol.

apiVersion: frp.zufardhiyaulhaq.com/v1alpha1
kind: Server
metadata:
  name: public-01
spec:
  bind:
    port: 7000                      # tcp/websocket clients; kcpPort / quicPort for kcp / quic
    tls:
      certificate:
        secret:
          name: frps-cert           # tls.crt, tls.key (+ ca.crt); unset = frps's random, unverifiable certificate
      # clientCA: {secret: {name: frps-client-ca}}   # mTLS
      # force: true                 # default; false only for wss behind a TLS terminator
  allowPorts:                       # remote ports for TCP/UDP Upstreams; nothing else is exposed
    tcp: ["8000-8010"]
    udp: ["5353"]
  vhost:
    httpPort: 80
    httpsPort: 443
    subdomainHost: example.com
  # authentication: {token: {secret: {name: my-token, key: token}}}   # unset = generated
  # restartPolicy: Automatic        # or Manual
  service:
    type: LoadBalancer              # or ClusterIP
    annotations:
      metallb.universe.tf/loadBalancerIPs: 203.0.113.10
  dashboard:
    enabled: true                   # UI + admin API on 7500, through its own Service only
  metrics:
    enabled: true                   # /metrics behind the dashboard login
Status Meaning
address The Client’s host: the load balancer’s IP or hostname, or <server>-frps.<namespace>.svc for ClusterIP
tls.serverNames DNS names of bind.tls.certificate; use one as the Client’s tls.serverName
exposedPorts The expanded allowPorts; a private ServerPool’s allowedPorts
connectionSecret <server>-connection with token (and ca.crt); copy it to private clusters
Ready True when the pod is ready and the address is set; otherwise PodStarting, PodFailed, AddressPending or LoadBalancerFailed
ConfigSynced True when frps runs the current config; otherwise InvalidConfig, Restarting or RestartPending

A static IP keeps private clusters’ host stable. Set it with your cloud’s Service annotation: service.beta.kubernetes.io/aws-load-balancer-eip-allocations (AWS), networking.gke.io/load-balancer-ip-addresses (GKE), service.beta.kubernetes.io/azure-load-balancer-ipv4 (Azure) or metallb.universe.tf/loadBalancerIPs (MetalLB). The Service is updated in place and never recreated, so its IP survives every change except deleting the Server.

Good to know:

See examples/server.

Monitoring

The operator exposes Prometheus metrics on the <release>-controller-manager-metrics-service Service (port http/8080 by default):

Metric Labels Description
frp_client_info namespace, client, server_address, server_port, client_id, frpc_image Always 1
frp_client_ready namespace, client 1 when the Ready condition is True
frp_client_config_synced namespace, client 1 when the ConfigSynced condition is True; 0 when the reload failed (ConfigReloadFailed) or the spec is invalid, e.g. a missing Secret (InvalidConfig, also emitted as a Warning event)
frp_client_upstreams / frp_client_visitors namespace, client Attached resource counts
frp_client_admin_up namespace, client 1 when the frpc admin API answered /api/status
frp_client_last_reconcile_timestamp_seconds namespace, client Unix time of the last reconcile
frp_proxy_status namespace, client, proxy, type, status One-hot frpc proxy phase (running, start error, check failed, closed, wait start, new)
frp_proxy_info namespace, client, proxy, type, local_addr, remote_addr, plugin Always 1
frp_loadbalancer_service namespace, service, pool, server, state Always 1; state is bound or pending
frp_serverpool_allocated_ports pool, server Ports allocated on the server

Enable scraping with metrics.serviceMonitor.enabled=true (Prometheus Operator; the VictoriaMetrics operator also consumes ServiceMonitor objects). With metrics.secure=true the scraper’s ServiceAccount must be allowed GET /metrics (bind it to the <release>-metrics-reader ClusterRole).

A Grafana dashboard lives at dashboards/frp-operator.json — import it into Grafana (Dashboards → New → Import) and pick your Prometheus/VictoriaMetrics datasource.

Values

Key Type Default Description
loadBalancer.enabled bool true Bind Service type=LoadBalancer with loadBalancerClass: frp.zufardhiyaulhaq.com/frp to ServerPool servers
metrics.secure bool false  
metrics.serviceMonitor.additionalLabels object {}  
metrics.serviceMonitor.enabled bool false  
metrics.serviceMonitor.interval string "30s"  
operator.image string "ghcr.io/zufardhiyaulhaq/frp-operator"  
operator.imagePullPolicy string "Always" Image pull policy for the operator. Always keeps moving tags such as main fresh; use IfNotPresent for a locally imported image (the e2e suite does).
operator.replica int 1  
operator.tag string "v0.13.0"  
resources.limits.cpu string "200m"  
resources.limits.memory string "100Mi"  
resources.requests.cpu string "100m"  
resources.requests.memory string "20Mi"  
server.maxPorts int 50 Maximum number of ports on a Server’s public Service (bind, vhost and allowPorts). Cloud load balancers limit listeners.

see example files here


Autogenerated from chart metadata using helm-docs v1.14.2