Add initial API server proxy

Co-authored-by: Shyam <shyam0904a@users.noreply.github.com>
This commit is contained in:
Philip Laine
2026-05-26 10:53:36 +02:00
co-authored by Shyam
parent 59af2d6933
commit 819166cb0b
13 changed files with 1311 additions and 0 deletions
+2
View File
@@ -0,0 +1,2 @@
**
!bin/*/netbird-kubeapi-proxy
+27
View File
@@ -0,0 +1,27 @@
# Binaries for programs and plugins
*.exe
*.exe~
*.dll
*.so
*.dylib
bin/*
Dockerfile.cross
# Test binary, built with `go test -c`
*.test
# Output of the go coverage tool, specifically when used with LiteIDE
*.out
# Go workspace file
go.work
# Kubernetes Generated files - skip generated files, except for vendored files
!vendor/**/zz_generated.*
# editor and IDE paraphernalia
.idea
.vscode
*.swp
*.swo
*~
+61
View File
@@ -0,0 +1,61 @@
version: "2"
linters:
default: none
enable:
- copyloopvar
- dupl
- errcheck
- ginkgolinter
- gocyclo
- govet
- ineffassign
- misspell
- nakedret
- prealloc
- revive
- staticcheck
- unconvert
- unparam
- unused
- importas
- goheader
settings:
revive:
rules:
- name: comment-spacings
staticcheck:
checks: ["all", "-ST1000", "-ST1003", "-ST1016", "-ST1020", "-ST1021", "-ST1022", "-QF1008", "-SA1019"]
importas:
alias:
- pkg: github.com/netbirdio/netbird/shared/management/client/rest
alias: netbird
no-extra-aliases: true
goheader:
template: |-
SPDX-License-Identifier: BSD-3-Clause
exclusions:
generated: lax
rules:
- linters:
- dupl
path: internal/*
paths:
- third_party$
- builtin$
- examples$
formatters:
enable:
- gci
- gofmt
settings:
gci:
sections:
- blank
- standard
- prefix(golang.org/x)
- default
- prefix(github.com/netbirdio)
- localmodule
no-inline-comments: true
no-prefix-comments: true
custom-order: true
+11
View File
@@ -0,0 +1,11 @@
FROM gcr.io/distroless/static:nonroot
ARG TARGETOS
ARG TARGETARCH
LABEL org.opencontainers.image.title="NetBird Kubernetes API Proxy" \
org.opencontainers.image.description="Authorization proxy of Kubernetes API server using NetBird connections as identities." \
org.opencontainers.image.source="https://github.com/netbirdio/netbird-kubeapi-proxy" \
org.opencontainers.image.vendor="NetBird" \
org.opencontainers.image.licenses="BSD-3-Clause"
COPY bin/${TARGETOS}-${TARGETARCH}/netbird-kubeapi-proxy /usr/local/bin/
USER 65532:65532
ENTRYPOINT ["netbird-kubeapi-proxy"]
+29
View File
@@ -0,0 +1,29 @@
IMG_REGISTRY ?= ghcr.io
IMG_REPOSITORY ?= netbirdio/netbird-kubeapi-proxy
IMG_TAG ?= dev
IMG_REF := $(IMG_REGISTRY)/$(IMG_REPOSITORY):$(IMG_TAG)
.PHONY: lint
lint:
@golangci-lint run ./...
.PHONY: build
build: bin/linux-$(shell go env GOARCH)/netbird-kubeapi-proxy
bin/linux-%/netbird-kubeapi-proxy: $(shell find internal) main.go go.mod go.sum
@CGO_ENABLED=0 GOOS=linux GOARCH=$* go build -ldflags="-w -s" -trimpath -o $@ main.go
.PHONY: build-image
build-image: build
@DOCKER_BUILDKIT=1 docker build -t ${IMG_REF} .
@echo ${IMG_REF}
.PHONY: build-image-multiarch
build-image-multiarch: bin/linux-amd64/netbird-kubeapi-proxy bin/linux-arm64/netbird-kubeapi-proxy
@DOCKER_BUILDKIT=1 docker build --platform linux/amd64,linux/arm64 -t ${IMG_REF} .
@echo ${IMG_REF}
deploy: build-image
kind load docker-image ${IMG_REF}
kustomize build manifests | kubectl apply -f -
kubectl rollout restart deployment/netbird-kubeapi-proxy
+21
View File
@@ -0,0 +1,21 @@
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
name: cluster-reader
rules:
- apiGroups: ["*"]
resources: ["*"]
verbs: ["get", "list", "watch"]
---
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRoleBinding
metadata:
name: kubernetes-reader-binding
subjects:
- kind: Group
name: kubernetes-read
apiGroup: rbac.authorization.k8s.io
roleRef:
kind: ClusterRole
name: cluster-reader
apiGroup: rbac.authorization.k8s.io
+135
View File
@@ -0,0 +1,135 @@
module github.com/netbirdio/netbird-kubeapi-proxy
go 1.25.5
toolchain go1.26.3
require (
github.com/netbirdio/netbird v0.71.2
golang.org/x/sync v0.20.0
)
require (
cunicu.li/go-rosenpass v0.4.0 // indirect
github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be // indirect
github.com/apapsch/go-jsonmerge/v2 v2.0.0 // indirect
github.com/aws/aws-sdk-go-v2 v1.38.3 // indirect
github.com/aws/aws-sdk-go-v2/config v1.31.6 // indirect
github.com/aws/aws-sdk-go-v2/credentials v1.18.10 // indirect
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.6 // indirect
github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.6 // indirect
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.6 // indirect
github.com/aws/aws-sdk-go-v2/internal/ini v1.8.3 // indirect
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.1 // indirect
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.6 // indirect
github.com/aws/aws-sdk-go-v2/service/route53 v1.42.3 // indirect
github.com/aws/aws-sdk-go-v2/service/sso v1.29.1 // indirect
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.34.2 // indirect
github.com/aws/aws-sdk-go-v2/service/sts v1.38.2 // indirect
github.com/aws/smithy-go v1.23.0 // indirect
github.com/caddyserver/certmagic v0.21.3 // indirect
github.com/caddyserver/zerossl v0.1.3 // indirect
github.com/cenkalti/backoff/v4 v4.3.0 // indirect
github.com/cilium/ebpf v0.15.0 // indirect
github.com/cloudflare/circl v1.3.3 // indirect
github.com/coder/websocket v1.8.14 // indirect
github.com/coreos/go-iptables v0.7.0 // indirect
github.com/creack/pty v1.1.24 // indirect
github.com/fsnotify/fsnotify v1.9.0 // indirect
github.com/gliderlabs/ssh v0.3.8 // indirect
github.com/go-ole/go-ole v1.3.0 // indirect
github.com/godbus/dbus/v5 v5.1.0 // indirect
github.com/golang-jwt/jwt/v5 v5.3.1 // indirect
github.com/golang/protobuf v1.5.4 // indirect
github.com/google/btree v1.1.2 // indirect
github.com/google/go-cmp v0.7.0 // indirect
github.com/google/gopacket v1.1.19 // indirect
github.com/google/nftables v0.3.0 // indirect
github.com/google/uuid v1.6.0 // indirect
github.com/gopacket/gopacket v1.1.1 // indirect
github.com/hashicorp/errwrap v1.1.0 // indirect
github.com/hashicorp/go-multierror v1.1.1 // indirect
github.com/hashicorp/go-version v1.7.0 // indirect
github.com/huin/goupnp v1.2.0 // indirect
github.com/jackpal/go-nat-pmp v1.0.2 // indirect
github.com/jmespath/go-jmespath v0.4.0 // indirect
github.com/klauspost/cpuid/v2 v2.2.10 // indirect
github.com/koron/go-ssdp v0.0.4 // indirect
github.com/kr/fs v0.1.0 // indirect
github.com/libdns/libdns v0.2.2 // indirect
github.com/libdns/route53 v1.5.0 // indirect
github.com/libp2p/go-nat v0.2.0 // indirect
github.com/libp2p/go-netroute v0.4.0 // indirect
github.com/lrh3321/ipset-go v0.0.0-20250619021614-54a0a98ace81 // indirect
github.com/lufia/plan9stats v0.0.0-20240513124658-fba389f38bae // indirect
github.com/mdlayher/genetlink v1.3.2 // indirect
github.com/mdlayher/netlink v1.7.3-0.20250113171957-fbb4dce95f42 // indirect
github.com/mdlayher/socket v0.5.1 // indirect
github.com/mholt/acmez/v2 v2.0.1 // indirect
github.com/miekg/dns v1.1.72 // indirect
github.com/mitchellh/hashstructure/v2 v2.0.2 // indirect
github.com/oapi-codegen/runtime v1.1.2 // indirect
github.com/petermattis/goid v0.0.0-20250303134427-723919f7f203 // indirect
github.com/pion/dtls/v2 v2.2.10 // indirect
github.com/pion/dtls/v3 v3.0.9 // indirect
github.com/pion/ice/v4 v4.0.0-00010101000000-000000000000 // indirect
github.com/pion/logging v0.2.4 // indirect
github.com/pion/mdns/v2 v2.0.7 // indirect
github.com/pion/randutil v0.1.0 // indirect
github.com/pion/stun/v2 v2.0.0 // indirect
github.com/pion/stun/v3 v3.1.0 // indirect
github.com/pion/transport/v2 v2.2.4 // indirect
github.com/pion/transport/v3 v3.1.1 // indirect
github.com/pion/turn/v3 v3.0.1 // indirect
github.com/pion/turn/v4 v4.1.1 // indirect
github.com/pkg/errors v0.9.1 // indirect
github.com/pkg/sftp v1.13.9 // indirect
github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55 // indirect
github.com/quic-go/quic-go v0.55.0 // indirect
github.com/shirou/gopsutil/v3 v3.24.4 // indirect
github.com/shoenig/go-m1cpu v0.2.1 // indirect
github.com/sirupsen/logrus v1.9.4 // indirect
github.com/skratchdot/open-golang v0.0.0-20200116055534-eef842397966 // indirect
github.com/things-go/go-socks5 v0.0.4 // indirect
github.com/ti-mo/conntrack v0.5.1 // indirect
github.com/ti-mo/netfilter v0.5.2 // indirect
github.com/tklauser/go-sysconf v0.3.15 // indirect
github.com/tklauser/numcpus v0.10.0 // indirect
github.com/vishvananda/netlink v1.3.1 // indirect
github.com/vishvananda/netns v0.0.5 // indirect
github.com/wlynxg/anet v0.0.5 // indirect
github.com/yusufpapurcu/wmi v1.2.4 // indirect
github.com/zcalusic/sysinfo v1.1.3 // indirect
github.com/zeebo/blake3 v0.2.3 // indirect
go.uber.org/multierr v1.11.0 // indirect
go.uber.org/zap v1.27.0 // indirect
golang.org/x/crypto v0.50.0 // indirect
golang.org/x/exp v0.0.0-20250620022241-b7579e27df2b // indirect
golang.org/x/mod v0.34.0 // indirect
golang.org/x/net v0.53.0 // indirect
golang.org/x/oauth2 v0.36.0 // indirect
golang.org/x/sys v0.43.0 // indirect
golang.org/x/text v0.36.0 // indirect
golang.org/x/time v0.15.0 // indirect
golang.org/x/tools v0.43.0 // indirect
golang.zx2c4.com/wintun v0.0.0-20230126152724-0fa3db229ce2 // indirect
golang.zx2c4.com/wireguard v0.0.0-20230704135630-469159ecf7d1 // indirect
golang.zx2c4.com/wireguard/wgctrl v0.0.0-20230429144221-925a1e7659e6 // indirect
golang.zx2c4.com/wireguard/windows v0.5.3 // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20260401024825-9d38bb4040a9 // indirect
google.golang.org/grpc v1.80.0 // indirect
google.golang.org/protobuf v1.36.11 // indirect
gopkg.in/natefinch/lumberjack.v2 v2.2.1 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
gvisor.dev/gvisor v0.0.0-20260219192049-0f2374377e89 // indirect
)
replace github.com/dexidp/dex => github.com/netbirdio/dex v0.244.1-0.20260512110716-8d70ad8647c1
replace github.com/cloudflare/circl => codeberg.org/cunicu/circl v0.0.0-20230801113412-fec58fc7b5f6
replace github.com/pion/ice/v4 => github.com/netbirdio/ice/v4 v4.0.0-20250908184934-6202be846b51
replace golang.zx2c4.com/wireguard => github.com/netbirdio/wireguard-go v0.0.0-20260107100953-33b7c9d03db0
replace github.com/netbirdio/netbird => github.com/netbirdio/netbird v0.71.5-0.20260525190024-d542c60e2182
+682
View File
File diff suppressed because it is too large Load Diff
+139
View File
@@ -0,0 +1,139 @@
// SPDX-License-Identifier: BSD-3-Clause
package proxy
import (
"context"
"crypto/ecdsa"
"crypto/elliptic"
"crypto/rand"
"crypto/tls"
"crypto/x509"
"crypto/x509/pkix"
"fmt"
"math/big"
"net"
"net/http"
"net/http/httputil"
"net/url"
"os"
"time"
"github.com/netbirdio/netbird/client/embed"
netbird "github.com/netbirdio/netbird/shared/management/client/rest"
)
func Server(embedClient *embed.Client, netbirdClient *netbird.Client, kubeAPIServerURL *url.URL) (*http.Server, error) {
saToken, err := os.ReadFile("/var/run/secrets/kubernetes.io/serviceaccount/token")
if err != nil {
return nil, err
}
bearerToken := string(saToken)
certPool, err := x509.SystemCertPool()
if err != nil {
return nil, err
}
k8sCA, err := os.ReadFile("/var/run/secrets/kubernetes.io/serviceaccount/ca.crt")
if err != nil {
return nil, err
}
if ok := certPool.AppendCertsFromPEM(k8sCA); !ok {
return nil, fmt.Errorf("failed to append Kubernetes CA certificate")
}
transport := http.DefaultTransport.(*http.Transport).Clone()
transport.TLSClientConfig = &tls.Config{
RootCAs: certPool,
}
proxy := &httputil.ReverseProxy{
Transport: transport,
Rewrite: func(pr *httputil.ProxyRequest) {
allowedHeaders := map[string]any{
"Accept": nil,
"Accept-Encoding": nil,
"Content-Length": nil,
"Content-Type": nil,
"User-Agent": nil,
}
for k := range pr.Out.Header {
if _, ok := allowedHeaders[k]; !ok {
pr.Out.Header.Del(k)
}
}
remoteIP, _, err := net.SplitHostPort(pr.In.RemoteAddr)
if err != nil {
return
}
listCtx, listCancel := context.WithTimeout(pr.In.Context(), 10*time.Second)
defer listCancel()
peers, err := netbirdClient.Peers.List(listCtx, netbird.PeerIPFilter(remoteIP))
if err != nil {
return
}
if len(peers) != 1 {
return
}
peer := peers[0]
pr.Out.Header.Set("Impersonate-User", peer.UserId)
for _, group := range peer.Groups {
pr.Out.Header.Add("Impersonate-Group", group.Name)
}
pr.Out.Header.Set("Authorization", "Bearer "+bearerToken)
pr.SetURL(kubeAPIServerURL)
},
}
stat, err := embedClient.Status()
if err != nil {
return nil, err
}
proxyCert, err := generateSelfSignedCert(stat.LocalPeerState.FQDN)
if err != nil {
return nil, err
}
srv := http.Server{
TLSConfig: &tls.Config{
Certificates: []tls.Certificate{proxyCert},
MinVersion: tls.VersionTLS12,
},
Handler: proxy,
ReadHeaderTimeout: 10 * time.Second,
IdleTimeout: 60 * time.Second,
}
return &srv, nil
}
func generateSelfSignedCert(fqdn string) (tls.Certificate, error) {
priv, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
if err != nil {
return tls.Certificate{}, err
}
serialNumber, err := rand.Int(rand.Reader, new(big.Int).Lsh(big.NewInt(1), 128))
if err != nil {
return tls.Certificate{}, err
}
template := x509.Certificate{
SerialNumber: serialNumber,
Subject: pkix.Name{
Organization: []string{"NetBird K8s Auth Proxy"},
CommonName: fqdn,
},
NotBefore: time.Now(),
NotAfter: time.Now().Add(365 * 24 * time.Hour),
KeyUsage: x509.KeyUsageKeyEncipherment | x509.KeyUsageDigitalSignature,
ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth},
BasicConstraintsValid: true,
DNSNames: []string{fqdn},
}
certDER, err := x509.CreateCertificate(rand.Reader, &template, &template, &priv.PublicKey, priv)
if err != nil {
return tls.Certificate{}, err
}
return tls.Certificate{
Certificate: [][]byte{certDER},
PrivateKey: priv,
}, nil
}
+111
View File
@@ -0,0 +1,111 @@
// SPDX-License-Identifier: BSD-3-Clause
package main
import (
"context"
"errors"
"flag"
"io"
"log"
"net/http"
"net/url"
"os"
"os/signal"
"syscall"
"golang.org/x/sync/errgroup"
"github.com/netbirdio/netbird/client/embed"
netbird "github.com/netbirdio/netbird/shared/management/client/rest"
"github.com/netbirdio/netbird-kubeapi-proxy/internal/proxy"
)
func main() {
var (
mgmtURL string
apiKey string
setupKey string
kubeAPIServer string
instanceName string
clusterName string
)
flag.StringVar(&mgmtURL, "management-url", "https://api.netbird.io", "NetBird management URL")
flag.StringVar(&apiKey, "api-key", os.Getenv("NB_API_KEY"), "NetBird API key")
flag.StringVar(&setupKey, "setup-key", os.Getenv("NB_SETUP_KEY"), "NetBird setup key")
flag.StringVar(&kubeAPIServer, "kubernetes-api-server", "https://kubernetes.default.svc.cluster.local", "Target Kubernetes API server URL")
flag.StringVar(&instanceName, "instance-name", "", "Name of the instance")
flag.StringVar(&clusterName, "cluster-name", "", "Name of the cluster")
flag.Parse()
err := run(context.Background(), kubeAPIServer, mgmtURL, apiKey, setupKey, instanceName, clusterName)
if err != nil {
log.Fatal(err)
}
}
func run(ctx context.Context, kubeAPIServer, mgmtURL, apiKey, setupKey, instanceName, clusterName string) error {
ctx, cancel := signal.NotifyContext(ctx, syscall.SIGTERM)
defer cancel()
g, gCtx := errgroup.WithContext(ctx)
kubeAPIServerURL, err := url.Parse(kubeAPIServer)
if err != nil {
return err
}
if kubeAPIServerURL.Scheme != "https" || kubeAPIServerURL.Host == "" {
return errors.New("kubernetes-api-server must be an absolute https URL")
}
netbirdClient := netbird.NewWithOptions(
netbird.WithManagementURL(mgmtURL),
netbird.WithBearerToken(apiKey),
)
opts := embed.Options{
ManagementURL: mgmtURL,
SetupKey: setupKey,
DeviceName: instanceName,
LogOutput: io.Discard,
DNSLabels: []string{clusterName + "." + "netbird-kubeapi-proxy"},
}
embedClient, err := embed.New(opts)
if err != nil {
return err
}
err = embedClient.Start(ctx)
if err != nil {
return err
}
g.Go(func() error {
<-gCtx.Done()
return embedClient.Stop(context.Background())
})
proxySrv, err := proxy.Server(embedClient, netbirdClient, kubeAPIServerURL)
if err != nil {
return err
}
listener, err := embedClient.ListenTCP(":443")
if err != nil {
return err
}
g.Go(func() error {
err := proxySrv.ServeTLS(listener, "", "")
if err != nil && !errors.Is(err, http.ErrServerClosed) {
return err
}
return nil
})
g.Go(func() error {
<-gCtx.Done()
return proxySrv.Shutdown(context.Background())
})
err = g.Wait()
if err != nil {
return err
}
return nil
}
+50
View File
@@ -0,0 +1,50 @@
apiVersion: apps/v1
kind: Deployment
metadata:
name: netbird-kubeapi-proxy
labels:
app.kubernetes.io/name: netbird-kubeapi-proxy
spec:
replicas: 1
selector:
matchLabels:
app.kubernetes.io/name: netbird-kubeapi-proxy
template:
metadata:
labels:
app.kubernetes.io/name: netbird-kubeapi-proxy
spec:
serviceAccountName: netbird-kubeapi-proxy
securityContext:
seccompProfile:
type: RuntimeDefault
containers:
- name: proxy
image: ghcr.io/netbirdio/netbird-kubeapi-proxy:dev
args:
- --api-key=$(API_KEY)
- --setup-key=$(SETUP_KEY)
- --instance-name=$(POD_NAME)
- --cluster-name=kubernetes
env:
- name: POD_NAME
valueFrom:
fieldRef:
fieldPath: metadata.name
- name: SETUP_KEY
valueFrom:
secretKeyRef:
name: netbird-kubeapi-proxy
key: setup-key
- name: API_KEY
valueFrom:
secretKeyRef:
name: netbird-kubeapi-proxy
key: api-key
securityContext:
allowPrivilegeEscalation: false
readOnlyRootFilesystem: true
runAsNonRoot: true
capabilities:
drop:
- ALL
+5
View File
@@ -0,0 +1,5 @@
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
resources:
- deployment.yaml
- rbac.yaml
+38
View File
@@ -0,0 +1,38 @@
apiVersion: v1
kind: ServiceAccount
metadata:
name: netbird-kubeapi-proxy
---
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
name: netbird-kubeapi-proxy
rules:
- apiGroups:
- ""
resources:
- users
- groups
verbs:
- impersonate
- apiGroups:
- authentication.k8s.io
resources:
- userextras/*
- uids
verbs:
- impersonate
---
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRoleBinding
metadata:
name: netbird-kubeapi-proxy
roleRef:
apiGroup: rbac.authorization.k8s.io
kind: ClusterRole
name: netbird-kubeapi-proxy
subjects:
- kind: ServiceAccount
name: netbird-kubeapi-proxy
namespace: default