Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/helm.yml
Original file line number Diff line number Diff line change
Expand Up @@ -40,4 +40,4 @@ jobs:
charts_dir: helm
env:
CR_TOKEN: "${{ secrets.GITHUB_TOKEN }}"
CR_RELEASE_NAME_TEMPLATE: "helm-v{{ .Version }}"
CR_RELEASE_NAME_TEMPLATE: "helm-{{ .Name }}-v{{ .Version }}"
29 changes: 29 additions & 0 deletions Dockerfile.auth-proxy
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
# Build the auth-proxy binary
FROM golang:1.24 AS builder

ARG TARGETOS=linux
ARG TARGETARCH

WORKDIR /workspace

# Copy go mod files
COPY go.mod go.sum ./

# Download dependencies
RUN go mod download

# Copy source code
COPY cmd/ cmd/

# Build the binary
RUN CGO_ENABLED=0 GOOS=${TARGETOS} GOARCH=${TARGETARCH} go build -ldflags="-w -s" -a -o auth-proxy ./cmd/auth-proxy

# Runtime image - using alpine because NetBird client requires
# system utilities (uname, etc.) for system detection
FROM alpine:3.19
RUN apk add --no-cache ca-certificates
WORKDIR /
COPY --from=builder /workspace/auth-proxy .
USER 65532:65532
ENTRYPOINT ["/auth-proxy"]

4 changes: 2 additions & 2 deletions Dockerfile.kubectl
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
FROM alpine:3 AS builder

RUN apk update && apk add curl
RUN apk update && apk add curl bash
RUN curl -LO "https://dl.k8s.io/release/$(curl -L -s https://dl.k8s.io/release/stable.txt)/bin/linux/amd64/kubectl" && \
chmod +x kubectl && \
mv kubectl /usr/local/bin/kubectl

FROM alpine:3 AS final

COPY --from=builder /usr/local/bin/kubectl /usr/local/bin/kubectl
COPY --from=builder /usr/local/bin/kubectl /usr/local/bin/kubectl
246 changes: 246 additions & 0 deletions cmd/auth-proxy/main.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,246 @@
package main

import (
"context"
"crypto/ecdsa"
"crypto/elliptic"
"crypto/rand"
"crypto/tls"
"crypto/x509"
"crypto/x509/pkix"
"flag"
"log"
"math/big"
"net"
"net/http"
"net/http/httputil"
"net/url"
"os"
"os/signal"
"syscall"
"time"

"github.com/netbirdio/netbird/client/embed"
)

func main() {
var (
mgmtURL string
setupKey string
target string
listenAddr string
statePath string
logLevel string
configPath string
deviceName string
dnsDomain string
)

flag.StringVar(&mgmtURL, "management-url", os.Getenv("NB_MANAGEMENT_URL"), "NetBird Management URL")
flag.StringVar(&setupKey, "setup-key", os.Getenv("NB_SETUP_KEY"), "NetBird Setup Key")
flag.StringVar(&target, "target", os.Getenv("KUBERNETES_API_SERVER"), "Target Kubernetes API Server URL")
flag.StringVar(&listenAddr, "listen-addr", ":443", "Address to listen on (within NetBird network)")
flag.StringVar(&statePath, "state-path", "/var/lib/netbird/state.json", "Path to NetBird state file")
flag.StringVar(&logLevel, "log-level", "info", "Log level")
flag.StringVar(&configPath, "config-path", "/etc/netbird/config.json", "Path to NetBird config file")
flag.StringVar(&deviceName, "hostname", os.Getenv("NB_HOSTNAME"), "Device name (hostname)")
flag.StringVar(&dnsDomain, "dns-domain", os.Getenv("NB_DNS_DOMAIN"), "NetBird DNS domain (e.g., netbird.cloud)")

flag.Parse()

if setupKey == "" && os.Getenv("NB_SETUP_KEY") == "" {
if _, err := os.Stat(configPath); os.IsNotExist(err) {
log.Fatal("Setup key is required for initial setup")
}
}

if target == "" {
target = "https://kubernetes.default.svc"
}

// 1. Initialize NetBird Client
opts := embed.Options{
SetupKey: setupKey,
ManagementURL: mgmtURL,
StatePath: statePath,
ConfigPath: configPath,
LogLevel: logLevel,
DeviceName: deviceName,
}

client, err := embed.New(opts)
if err != nil {
log.Fatalf("Failed to create NetBird client: %v", err)
}

ctx, cancel := context.WithCancel(context.Background())
defer cancel()

sigCh := make(chan os.Signal, 1)
signal.Notify(sigCh, syscall.SIGINT, syscall.SIGTERM)
go func() {
<-sigCh
log.Println("Received signal, shutting down...")
cancel()
client.Stop(context.Background())
os.Exit(0)
}()

log.Println("Starting NetBird client...")
if err := client.Start(ctx); err != nil {
log.Fatalf("Failed to start NetBird client: %v", err)
}

// 2. Setup Reverse Proxy
targetURL, err := url.Parse(target)
if err != nil {
log.Fatalf("Invalid target URL: %v", err)
}

log.Printf("Proxying to %s", targetURL)

proxy := httputil.NewSingleHostReverseProxy(targetURL)

rootCAs, _ := x509.SystemCertPool()
if rootCAs == nil {
rootCAs = x509.NewCertPool()
}

k8sCA, err := os.ReadFile("/var/run/secrets/kubernetes.io/serviceaccount/ca.crt")
if err == nil {
rootCAs.AppendCertsFromPEM(k8sCA)
}

saToken, err := os.ReadFile("/var/run/secrets/kubernetes.io/serviceaccount/token")
if err != nil {
log.Printf("Warning: Could not load ServiceAccount token: %v", err)
}
bearerToken := string(saToken)

transport := &http.Transport{
TLSClientConfig: &tls.Config{
RootCAs: rootCAs,
},
}
proxy.Transport = transport

originalDirector := proxy.Director
proxy.Director = func(req *http.Request) {
originalDirector(req)

if bearerToken != "" {
req.Header.Set("Authorization", "Bearer "+bearerToken)
}

remoteIP, _, err := net.SplitHostPort(req.RemoteAddr)
if err != nil {
remoteIP = req.RemoteAddr
}

identity, err := client.WhoIs(remoteIP)
if err != nil {
log.Printf("Authentication failed: could not look up peer for IP %s: %v", remoteIP, err)
req.Header.Del("Impersonate-User")
req.Header.Del("Impersonate-Group")
return
}

// Use UserId if available, otherwise fall back to FQDN as username
username := identity.UserId
if username == "" {
username = identity.FQDN
}

log.Printf("Authenticated peer: %s (User: %s, Groups: %v)", identity.FQDN, username, identity.Groups)

req.Header.Set("Impersonate-User", username)
req.Header.Del("Impersonate-Group")
for _, group := range identity.Groups {
req.Header.Add("Impersonate-Group", group)
}
}

authHandler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
remoteIP, _, err := net.SplitHostPort(r.RemoteAddr)
if err != nil {
remoteIP = r.RemoteAddr
}

_, err = client.WhoIs(remoteIP)
if err != nil {
log.Printf("Access denied for %s: %v", remoteIP, err)
http.Error(w, "Unauthorized: Unknown NetBird Peer", http.StatusUnauthorized)
return
}

proxy.ServeHTTP(w, r)
})

// 3. Generate self-signed TLS certificate
tlsCert, err := generateSelfSignedCert(deviceName, dnsDomain)
if err != nil {
log.Fatalf("Failed to generate TLS certificate: %v", err)
}

tlsConfig := &tls.Config{
Certificates: []tls.Certificate{tlsCert},
MinVersion: tls.VersionTLS12,
}

// 4. Listen on NetBird Network with TLS
listener, err := client.ListenTCP(listenAddr)
if err != nil {
log.Fatalf("Failed to listen on %s: %v", listenAddr, err)
}

tlsListener := tls.NewListener(listener, tlsConfig)

log.Printf("Listening on %s (NetBird Network) with TLS", listenAddr)
if err := http.Serve(tlsListener, authHandler); err != nil {
log.Fatalf("Server error: %v", err)
}
}

// generateSelfSignedCert creates a self-signed certificate for the proxy
func generateSelfSignedCert(hostname, dnsDomain 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
}

// Build DNS names for the certificate
dnsNames := []string{hostname, "localhost"}
if dnsDomain != "" {
dnsNames = append(dnsNames, hostname+"."+dnsDomain)
}

template := x509.Certificate{
SerialNumber: serialNumber,
Subject: pkix.Name{
Organization: []string{"NetBird K8s Auth Proxy"},
CommonName: hostname,
},
NotBefore: time.Now(),
NotAfter: time.Now().Add(365 * 24 * time.Hour),
KeyUsage: x509.KeyUsageKeyEncipherment | x509.KeyUsageDigitalSignature,
ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth},
BasicConstraintsValid: true,
DNSNames: dnsNames,
IPAddresses: []net.IP{net.ParseIP("127.0.0.1")},
}

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
}
2 changes: 2 additions & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ godebug default=go1.23
require (
github.com/go-logr/logr v1.4.2
github.com/google/uuid v1.6.0
// NOTE: The auth-proxy requires netbird with embed client support.
// Update to the version that includes github.com/netbirdio/netbird/client/embed
github.com/netbirdio/netbird v0.36.7
github.com/onsi/ginkgo/v2 v2.21.0
github.com/onsi/gomega v1.35.1
Expand Down
10 changes: 10 additions & 0 deletions helm/kubernetes-operator/templates/_helpers.tpl
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,16 @@ Create the name of the service account to use
{{- end }}
{{- end }}

{{/*
Create the name of the service account to use for auth proxy
*/}}
{{- define "kubernetes-operator.authProxyServiceAccountName" -}}
{{- if .Values.authProxy.serviceAccount.create }}
{{- default (printf "%s-auth-proxy" (include "kubernetes-operator.fullname" .)) .Values.authProxy.serviceAccount.name }}
{{- else }}
{{- default "default" .Values.authProxy.serviceAccount.name }}
{{- end }}
{{- end }}

{{/*
Create the name of the webhook service
Expand Down
62 changes: 62 additions & 0 deletions helm/kubernetes-operator/templates/auth-proxy-deployment.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
{{- if .Values.authProxy.enabled -}}
apiVersion: apps/v1
kind: Deployment
metadata:
name: {{ include "kubernetes-operator.fullname" . }}-auth-proxy
namespace: {{ .Release.Namespace }}
labels:
{{- include "kubernetes-operator.labels" . | nindent 4 }}
app.kubernetes.io/component: auth-proxy
spec:
replicas: {{ .Values.authProxy.replicaCount }}
selector:
matchLabels:
{{- include "kubernetes-operator.selectorLabels" . | nindent 6 }}
app.kubernetes.io/component: auth-proxy
template:
metadata:
labels:
{{- include "kubernetes-operator.selectorLabels" . | nindent 8 }}
app.kubernetes.io/component: auth-proxy
spec:
serviceAccountName: {{ include "kubernetes-operator.authProxyServiceAccountName" . }}
containers:
- name: auth-proxy
image: "{{ .Values.authProxy.image.repository }}:{{ .Values.authProxy.image.tag }}"
imagePullPolicy: {{ .Values.authProxy.image.pullPolicy }}
args:
- "--config-path=/var/lib/netbird/config.json"
- "--state-path=/var/lib/netbird/state.json"
env:
- name: NB_MANAGEMENT_URL
value: {{ .Values.authProxy.managementURL | default "https://api.netbird.io" }}
- name: NB_HOSTNAME
value: {{ .Values.authProxy.hostname }}
- name: KUBERNETES_API_SERVER
value: "https://kubernetes.default.svc"
- name: NB_LOG_LEVEL
value: {{ .Values.authProxy.logLevel | default "info" }}
{{- if .Values.authProxy.dnsDomain }}
- name: NB_DNS_DOMAIN
value: {{ .Values.authProxy.dnsDomain }}
{{- end }}
- name: NB_SETUP_KEY
valueFrom:
secretKeyRef:
{{- if .Values.authProxy.existingSecret }}
name: {{ .Values.authProxy.existingSecret }}
key: NB_SETUP_KEY
{{- else }}
name: {{ include "kubernetes-operator.fullname" . }}-auth-proxy-secret
key: NB_SETUP_KEY
{{- end }}
resources:
{{- toYaml .Values.authProxy.resources | nindent 12 }}
volumeMounts:
- name: state
mountPath: /var/lib/netbird
volumes:
- name: state
emptyDir: {}
{{- end }}

Loading