|
| 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": ®istrysessions.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 | +} |
0 commit comments