Skip to content

Commit ad44a89

Browse files
authored
Merge pull request #44 from datum-cloud/feat/zitadelapiserver
feat: zitadel identity api server
2 parents b4b5488 + 68fd572 commit ad44a89

File tree

16 files changed

+950
-157
lines changed

16 files changed

+950
-157
lines changed

README.md

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,46 @@ workflows?"*
4747
7. **Integration Bridge**: Seamless integration with Milo's Kubernetes-based
4848
APIs
4949

50+
## Zitadel API Server (virtual Sessions)
51+
52+
This repository includes a small API server that exposes Milo's identity sessions as a Kubernetes-native API under the provider group/version:
53+
54+
- Group/Version: `identity.milo.io/v1alpha1`
55+
- Resource: `sessions`
56+
- Scope: cluster-scoped, virtual (no etcd)
57+
- Types: reuses Milo Identity public `Session` types bound to the provider G/V
58+
59+
### What it does
60+
61+
- Trusts Milo's inbound request headers (X-Remote-User, X-Remote-Group, X-Remote-Uid, etc)
62+
- Enforces self-scoping (users only see and act on their own sessions)
63+
- Proxies list/get/delete to Zitadel Session Service v2 using the official `zitadel-go/v3` SDK
64+
65+
### Deploy
66+
67+
Kustomize base manifests live under `config/base/services/apiserver/` and are included in `config/base/kustomization.yaml`.
68+
69+
- Deployment: runs the `apiserver` subcommand from this binary
70+
- Service: ClusterIP on 443 -> container 8443
71+
72+
Environment variables (mounted via Secret/ConfigMap as you prefer):
73+
74+
- `ZITADEL_API`: e.g. `<tenant>.<region>.zitadel.cloud`
75+
- `ZITADEL_ISSUER`: e.g. `https://<tenant>.<region>.zitadel.cloud`
76+
- `ZITADEL_KEY_PATH`: path to Zitadel machine account JSON key (mounted to the container)
77+
- `REQUESTHEADER_CLIENT_CA_FILE`: path to PEM CA bundle that signs Milo's client cert
78+
- `REQUESTHEADER_ALLOWED_NAMES`: allowed CNs for Milo client cert; empty means any signed by CA
79+
- `REQUESTHEADER_EXTRA_HEADERS_PREFIX`: header name prefixes to determine user extra info
80+
- `REQUESTHEADER_GROUP_HEADERS`: header names to determine user groups
81+
- `REQUESTHEADER_USERNAME_HEADERS`: header names to determine user identity
82+
- `REQUESTHEADER_UID_HEADERS`: header names to determine user UID
83+
84+
### Notes
85+
86+
- The apiserver is stateless and does not use etcd
87+
- It relies on the core apiserver for authentication and authorization
88+
- The service user (machine account JSON key) is used to authenticate to Zitadel
89+
5090
## Testing
5191

5292
Follow these steps to run the end-to-end (e2e) tests locally:

cmd/apiserver/command.go

Lines changed: 175 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,175 @@
1+
package zitadelapiserver
2+
3+
import (
4+
"context"
5+
"flag"
6+
"fmt"
7+
8+
"github.com/spf13/cobra"
9+
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
10+
"k8s.io/apimachinery/pkg/runtime"
11+
"k8s.io/apimachinery/pkg/runtime/serializer"
12+
"k8s.io/apiserver/pkg/apis/apiserver"
13+
authorizerfactory "k8s.io/apiserver/pkg/authorization/authorizerfactory"
14+
openapi "k8s.io/apiserver/pkg/endpoints/openapi"
15+
"k8s.io/apiserver/pkg/registry/rest"
16+
genericserver "k8s.io/apiserver/pkg/server"
17+
genericoptions "k8s.io/apiserver/pkg/server/options"
18+
clientgoscheme "k8s.io/client-go/kubernetes/scheme"
19+
compatibility "k8s.io/component-base/compatibility"
20+
"k8s.io/klog/v2"
21+
openapicommon "k8s.io/kube-openapi/pkg/common"
22+
generatedopenapi "k8s.io/kubernetes/pkg/generated/openapi"
23+
24+
registrysessions "go.miloapis.com/auth-provider-zitadel/internal/apiserver/identity/sessions"
25+
"go.miloapis.com/auth-provider-zitadel/internal/config"
26+
identityinstall "go.miloapis.com/auth-provider-zitadel/pkg/apis/identity"
27+
"go.miloapis.com/auth-provider-zitadel/pkg/zitadel"
28+
miloidentity "go.miloapis.com/milo/pkg/apis/identity"
29+
identityv1alpha1 "go.miloapis.com/milo/pkg/apis/identity/v1alpha1"
30+
logf "sigs.k8s.io/controller-runtime/pkg/log"
31+
)
32+
33+
// NewAPIServerCommand creates a cobra command that runs the aggregated API server
34+
// for the identity.miloapis.com/v1alpha1 group.
35+
func NewAPIServerCommand(global *config.GlobalConfig) *cobra.Command {
36+
log := logf.Log.WithName("apiserver-cmd")
37+
38+
var (
39+
tlsCertFile string
40+
tlsKeyFile string
41+
securePort int
42+
// RequestHeader front-proxy trust configuration
43+
requestHeaderCAFile string
44+
requestHeaderAllowedNames []string
45+
requestHeaderUsernameHeaders []string
46+
requestHeaderGroupHeaders []string
47+
requestHeaderUIDHeaders []string
48+
requestHeaderExtraHeadersPref []string
49+
// Zitadel configuration (flags with env fallbacks)
50+
zitadelIssuer string
51+
zitadelAPI string
52+
zitadelKeyPath string
53+
)
54+
55+
cmd := &cobra.Command{
56+
Use: "apiserver",
57+
Short: "Run API server for Zitadel sessions",
58+
RunE: func(cmd *cobra.Command, args []string) error {
59+
if err := config.InitializeLogging(global); err != nil {
60+
return fmt.Errorf("init logging: %w", err)
61+
}
62+
// Route klog through the controller-runtime logger and ensure flushing on exit
63+
klog.EnableContextualLogging(true)
64+
defer klog.Flush()
65+
log.Info("Starting API server")
66+
67+
scheme := runtime.NewScheme()
68+
identityinstall.Install(scheme)
69+
miloidentity.Install(scheme)
70+
_ = clientgoscheme.AddToScheme(scheme)
71+
72+
codecs := serializer.NewCodecFactory(scheme)
73+
74+
ro := genericoptions.NewRecommendedOptions("/unused/registry", nil)
75+
// Ensure we don't try to bind to privileged port 443; default to 8443 and allow override via flag
76+
ro.SecureServing.BindPort = securePort
77+
if tlsCertFile != "" && tlsKeyFile != "" {
78+
ro.SecureServing.ServerCert.CertKey.CertFile = tlsCertFile
79+
ro.SecureServing.ServerCert.CertKey.KeyFile = tlsKeyFile
80+
}
81+
// Configure RequestHeader authn to trust Milo as a front-proxy
82+
authn := genericoptions.NewDelegatingAuthenticationOptions()
83+
authn.SkipInClusterLookup = true
84+
authn.Anonymous = &apiserver.AnonymousAuthConfig{Enabled: false}
85+
authn.RequestHeader.ClientCAFile = requestHeaderCAFile
86+
authn.RequestHeader.AllowedNames = requestHeaderAllowedNames
87+
authn.RequestHeader.UsernameHeaders = requestHeaderUsernameHeaders
88+
authn.RequestHeader.GroupHeaders = requestHeaderGroupHeaders
89+
authn.RequestHeader.UIDHeaders = requestHeaderUIDHeaders
90+
authn.RequestHeader.ExtraHeaderPrefixes = requestHeaderExtraHeadersPref
91+
ro.Authentication = authn
92+
// Use an allow-all authorizer so Milo acts as PDP
93+
ro.Authorization = nil
94+
ro.Etcd = nil
95+
ro.Admission = nil
96+
ro.CoreAPI = nil
97+
ro.Audit = nil
98+
ro.Features.EnablePriorityAndFairness = false
99+
100+
cfg := genericserver.NewRecommendedConfig(codecs)
101+
if err := ro.ApplyTo(cfg); err != nil {
102+
return fmt.Errorf("apply recommended options: %w", err)
103+
}
104+
105+
// Always-allow authorizer (treat Milo front-proxy as PDP)
106+
cfg.Authorization.Authorizer = authorizerfactory.NewAlwaysAllowAuthorizer()
107+
// Ensure EffectiveVersion is non-nil to avoid nil deref in Complete()
108+
if cfg.EffectiveVersion == nil {
109+
cfg.EffectiveVersion = compatibility.NewEffectiveVersionFromString("", "", "")
110+
}
111+
// Enable OpenAPI and provide minimal definitions set
112+
cfg.SkipOpenAPIInstallation = false
113+
getOpenAPIDefinitions := func(ref openapicommon.ReferenceCallback) map[string]openapicommon.OpenAPIDefinition {
114+
base := generatedopenapi.GetOpenAPIDefinitions(ref)
115+
id := identityv1alpha1.GetOpenAPIDefinitions(ref)
116+
for k, v := range id {
117+
base[k] = v
118+
}
119+
return base
120+
}
121+
cfg.OpenAPIConfig = genericserver.DefaultOpenAPIConfig(getOpenAPIDefinitions, openapi.NewDefinitionNamer(scheme))
122+
cfg.OpenAPIConfig.Info.Title = "Milo Sessions API"
123+
cfg.OpenAPIConfig.Info.Version = "v1alpha1"
124+
cfg.OpenAPIV3Config = genericserver.DefaultOpenAPIV3Config(getOpenAPIDefinitions, openapi.NewDefinitionNamer(scheme))
125+
126+
// Note: secure serving is configured by flags of the apiserver library; we default to its settings.
127+
128+
srv, err := cfg.Complete().New("zitadel-sessions-apiserver", genericserver.NewEmptyDelegate())
129+
if err != nil {
130+
return fmt.Errorf("build server: %w", err)
131+
}
132+
133+
zc, err := zitadel.NewSDK(context.Background(), zitadel.SDKConfig{
134+
Issuer: zitadelIssuer,
135+
Domain: zitadelAPI,
136+
KeyPath: zitadelKeyPath,
137+
})
138+
if err != nil {
139+
return fmt.Errorf("init zitadel sdk: %w", err)
140+
}
141+
142+
storage := map[string]rest.Storage{"sessions": &registrysessions.REST{Z: zc}}
143+
144+
agi := genericserver.NewDefaultAPIGroupInfo(identityv1alpha1.SchemeGroupVersion.Group, scheme, metav1.ParameterCodec, codecs)
145+
agi.VersionedResourcesStorageMap = map[string]map[string]rest.Storage{"v1alpha1": storage}
146+
if err := srv.InstallAPIGroup(&agi); err != nil {
147+
return fmt.Errorf("install api group: %w", err)
148+
}
149+
150+
log.Info("API server is starting")
151+
return srv.PrepareRun().RunWithContext(cmd.Context())
152+
},
153+
}
154+
155+
cmd.Flags().StringVar(&tlsCertFile, "tls-cert-file", "", "Path to TLS certificate")
156+
cmd.Flags().StringVar(&tlsKeyFile, "tls-private-key-file", "", "Path to TLS private key")
157+
cmd.Flags().IntVar(&securePort, "secure-port", 8443, "Secure serving port")
158+
// RequestHeader trust configuration flags
159+
cmd.Flags().StringVar(&requestHeaderCAFile, "requestheader-client-ca-file", "", "Path to PEM CA bundle that signs Milo's proxy client cert")
160+
cmd.Flags().StringSliceVar(&requestHeaderAllowedNames, "requestheader-allowed-names", nil, "Allowed CNs for Milo proxy client cert; empty means any signed by CA")
161+
cmd.Flags().StringSliceVar(&requestHeaderUsernameHeaders, "requestheader-username-headers", nil, "Header names to determine user identity")
162+
cmd.Flags().StringSliceVar(&requestHeaderGroupHeaders, "requestheader-group-headers", nil, "Header names to determine user groups")
163+
cmd.Flags().StringSliceVar(&requestHeaderUIDHeaders, "requestheader-uid-headers", nil, "Header names to determine user UID")
164+
cmd.Flags().StringSliceVar(&requestHeaderExtraHeadersPref, "requestheader-extra-headers-prefix", nil, "Header name prefixes to determine user extra info")
165+
cmd.Flags().StringVar(&zitadelIssuer, "zitadel-issuer", "", "Zitadel issuer URL")
166+
cmd.Flags().StringVar(&zitadelAPI, "zitadel-api", "", "Zitadel API base URL")
167+
cmd.Flags().StringVar(&zitadelKeyPath, "zitadel-key", "", "Path to Zitadel machine account key")
168+
169+
// Wire klog flags to this command so users can set verbosity with -v=N
170+
goFS := flag.NewFlagSet("klog", flag.ContinueOnError)
171+
klog.InitFlags(goFS)
172+
cmd.Flags().AddGoFlagSet(goFS)
173+
174+
return cmd
175+
}

cmd/main.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import (
77
"github.com/spf13/cobra"
88

99
"go.miloapis.com/auth-provider-zitadel/cmd/actionsserver"
10+
zitadelapiserver "go.miloapis.com/auth-provider-zitadel/cmd/apiserver"
1011
"go.miloapis.com/auth-provider-zitadel/cmd/controller"
1112
"go.miloapis.com/auth-provider-zitadel/cmd/version"
1213
"go.miloapis.com/auth-provider-zitadel/cmd/webhookserver"
@@ -49,6 +50,7 @@ func execute() error {
4950
rootCmd.AddCommand(version.NewVersionCommand())
5051
rootCmd.AddCommand(actionsserver.NewActionsServerCommand(globalConfig))
5152
rootCmd.AddCommand(webhookserver.NewAuthenticationWebhookServerCommand(globalConfig))
53+
rootCmd.AddCommand(zitadelapiserver.NewAPIServerCommand(globalConfig))
5254

5355
return rootCmd.Execute()
5456
}

config/base/kustomization.yaml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,3 +11,4 @@ resources:
1111
- rbac
1212
- services/controller-manager
1313
- services/authn-webhook
14+
- services/apiserver
Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,94 @@
1+
apiVersion: apps/v1
2+
kind: Deployment
3+
metadata:
4+
name: apiserver
5+
labels:
6+
app.kubernetes.io/name: auth-provider-zitadel
7+
app.kubernetes.io/component: apiserver
8+
spec:
9+
replicas: 1
10+
selector:
11+
matchLabels:
12+
app.kubernetes.io/name: auth-provider-zitadel
13+
app.kubernetes.io/component: apiserver
14+
template:
15+
metadata:
16+
labels:
17+
app.kubernetes.io/name: auth-provider-zitadel
18+
app.kubernetes.io/component: apiserver
19+
spec:
20+
securityContext:
21+
runAsNonRoot: true
22+
seccompProfile:
23+
type: RuntimeDefault
24+
containers:
25+
- name: apiserver
26+
image: ghcr.io/datum-cloud/auth-provider-zitadel:latest
27+
args:
28+
- apiserver
29+
- --secure-port=8443
30+
- --tls-cert-file=$(TLS_CERT_FILE)
31+
- --tls-private-key-file=$(TLS_PRIVATE_KEY_FILE)
32+
- --requestheader-client-ca-file=$(REQUESTHEADER_CLIENT_CA_FILE)
33+
- --requestheader-allowed-names=$(REQUESTHEADER_ALLOWED_NAMES)
34+
- --requestheader-extra-headers-prefix=$(REQUESTHEADER_EXTRA_HEADERS_PREFIX)
35+
- --requestheader-group-headers=$(REQUESTHEADER_GROUP_HEADERS)
36+
- --requestheader-username-headers=$(REQUESTHEADER_USERNAME_HEADERS)
37+
- --requestheader-uid-headers=$(REQUESTHEADER_UID_HEADERS)
38+
- --zitadel-issuer=$(ZITADEL_ISSUER)
39+
- --zitadel-api=$(ZITADEL_API)
40+
- --zitadel-key=$(ZITADEL_MACHINE_ACCOUNT_KEY_PATH)
41+
- -v=4
42+
env:
43+
- name: LOG_LEVEL
44+
value: "info"
45+
- name: LOG_FORMAT
46+
value: "json"
47+
- name: ZITADEL_ISSUER
48+
value: "https://zitadel.datum-cloud.com"
49+
- name: ZITADEL_API
50+
value: "zitadel.datum-cloud.com"
51+
- name: ZITADEL_MACHINE_ACCOUNT_KEY_PATH
52+
value: "/etc/zitadel/machine-account-key.json"
53+
- name: TLS_CERT_FILE
54+
value: /etc/kubernetes/pki/client/tls.crt
55+
- name: TLS_PRIVATE_KEY_FILE
56+
value: /etc/kubernetes/pki/client/tls.key
57+
- name: REQUESTHEADER_CLIENT_CA_FILE
58+
value: "/etc/kubernetes/pki/trust/ca.crt"
59+
- name: REQUESTHEADER_ALLOWED_NAMES
60+
value: ""
61+
- name: REQUESTHEADER_EXTRA_HEADERS_PREFIX
62+
value: "X-Remote-Extra-"
63+
- name: REQUESTHEADER_USERNAME_HEADERS
64+
value: "X-Remote-User"
65+
- name: REQUESTHEADER_GROUP_HEADERS
66+
value: "X-Remote-Group"
67+
- name: REQUESTHEADER_UID_HEADERS
68+
value: "X-Remote-Uid"
69+
ports:
70+
- name: https
71+
containerPort: 8443
72+
protocol: TCP
73+
livenessProbe:
74+
httpGet:
75+
path: /livez
76+
port: 8443
77+
scheme: HTTPS
78+
initialDelaySeconds: 10
79+
periodSeconds: 20
80+
readinessProbe:
81+
httpGet:
82+
path: /readyz
83+
port: 8443
84+
scheme: HTTPS
85+
initialDelaySeconds: 5
86+
periodSeconds: 10
87+
securityContext:
88+
allowPrivilegeEscalation: false
89+
capabilities:
90+
drop: ["ALL"]
91+
volumeMounts: []
92+
volumes: []
93+
94+
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
apiVersion: kustomize.config.k8s.io/v1beta1
2+
kind: Kustomization
3+
4+
resources:
5+
- deployment.yaml
6+
- service.yaml
7+
8+
9+
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
apiVersion: v1
2+
kind: Service
3+
metadata:
4+
name: apiserver
5+
namespace: auth-provider-zitadel-system
6+
labels:
7+
app.kubernetes.io/name: auth-provider-zitadel
8+
app.kubernetes.io/component: apiserver
9+
spec:
10+
selector:
11+
app.kubernetes.io/name: auth-provider-zitadel
12+
app.kubernetes.io/component: apiserver
13+
ports:
14+
- name: https
15+
port: 443
16+
targetPort: 8443
17+
protocol: TCP
18+
19+

config/default/kustomization.yaml

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -32,12 +32,12 @@ resources:
3232
#- ../network-policy
3333

3434
# Uncomment the patches line if you enable Metrics
35-
patches:
35+
# patches:
3636
# [METRICS] The following patch will enable the metrics endpoint using HTTPS and the port :8443.
3737
# More info: https://book.kubebuilder.io/reference/metrics
38-
- path: manager_metrics_patch.yaml
39-
target:
40-
kind: Deployment
38+
# - path: manager_metrics_patch.yaml
39+
# target:
40+
# kind: Deployment
4141

4242
# Uncomment the patches line if you enable Metrics and CertManager
4343
# [METRICS-WITH-CERTS] To enable metrics protected with certManager, uncomment the following line.

config/default/manager_metrics_patch.yaml

Lines changed: 0 additions & 4 deletions
This file was deleted.

0 commit comments

Comments
 (0)