// keycape is the main server binary for the KeyCape IAM profile service. // It orchestrates Authelia, LLDAP, and privacyIDEA to implement the // NetKingdom IAM Profile (OIDC/PKCE Authorization Code Flow). package main import ( "context" "crypto/rsa" "crypto/x509" "encoding/json" "encoding/pem" "flag" "fmt" "net/http" "os" "strings" "time" "github.com/rs/zerolog" "keycape/internal/adapters/authelia" "keycape/internal/adapters/lldap" "keycape/internal/adapters/privacyidea" "keycape/internal/config" "keycape/internal/domain" servererrors "keycape/internal/server/errors" "keycape/internal/server/oidc" "keycape/internal/server/telemetry" ) const version = "0.1.0" func main() { log := zerolog.New(os.Stdout).With().Timestamp().Logger() // ----------------------------------------------------------------- // 1. Parse flags and load config. // ----------------------------------------------------------------- var cfgPath string flag.StringVar(&cfgPath, "config", "", "path to YAML config file (env: KEYCAPE_CONFIG)") flag.Parse() cfg, err := config.Load(cfgPath) if err != nil { log.Error().Err(err).Msg("failed to load config") os.Exit(1) } // ----------------------------------------------------------------- // 2. Validate config. // ----------------------------------------------------------------- errs := config.ValidateConfig(cfg) if len(errs) > 0 { log.Error().Strs("errors", errs).Msg("config validation failed") os.Exit(1) } // ----------------------------------------------------------------- // 3. Load RSA private key. // ----------------------------------------------------------------- privateKey, err := loadPrivateKey(cfg.PrivateKeyPEM) if err != nil { log.Error().Err(err).Str("path", cfg.PrivateKeyPEM).Msg("failed to load private key") os.Exit(1) } // ----------------------------------------------------------------- // 4. Build JWKS from public key. // ----------------------------------------------------------------- ks := oidc.NewKeySet() ks.AddKey("key-1", &privateKey.PublicKey) // ----------------------------------------------------------------- // 5. Build client registry. // ----------------------------------------------------------------- clients, err := buildClientRegistry(cfg.Clients) if err != nil { log.Error().Err(err).Msg("failed to build client registry") os.Exit(1) } // ----------------------------------------------------------------- // 6. Create adapters. // ----------------------------------------------------------------- lldapAdapter := lldap.New(cfg.LLDAP) autheliaAdapter := authelia.New(cfg.Authelia, nil) privacyIDEAAdapter := privacyidea.New(cfg.PrivacyIDEA, nil) // ----------------------------------------------------------------- // 7. Create telemetry emitter. // ----------------------------------------------------------------- emitter := telemetry.NewLogEmitter(log) // ----------------------------------------------------------------- // 8. Create enforcement registry. // ----------------------------------------------------------------- enforcement := servererrors.DefaultRegistry() // ----------------------------------------------------------------- // 9. Create session store. // ----------------------------------------------------------------- sessions := oidc.NewSessionStore() // ----------------------------------------------------------------- // 10. Parse token lifetime. // ----------------------------------------------------------------- tokenLifetime := 15 * time.Minute if cfg.TokenLifetime != "" { d, err := time.ParseDuration(cfg.TokenLifetime) if err != nil { log.Error().Err(err).Str("tokenLifetime", cfg.TokenLifetime).Msg("invalid tokenLifetime") os.Exit(1) } tokenLifetime = d } // ----------------------------------------------------------------- // 11. Build issuer base URL. // ----------------------------------------------------------------- issuer := strings.TrimRight(cfg.Issuer, "/") // ----------------------------------------------------------------- // 12. Register HTTP handlers. // ----------------------------------------------------------------- mux := http.NewServeMux() // Discovery. mux.Handle("/.well-known/openid-configuration", oidc.NewDiscoveryHandler(oidc.DiscoveryConfig{ Issuer: issuer, AuthorizationEndpoint: issuer + "/authorize", TokenEndpoint: issuer + "/token", JWKSUri: issuer + "/jwks", UserinfoEndpoint: issuer + "/userinfo", })) // JWKS. mux.Handle("/jwks", oidc.NewJWKSHandler(ks)) // Authorize handler (with enforcement middleware). authorizeHandler := &oidc.AuthorizeHandler{ ClientConfig: clients, Auth: autheliaAdapter, MFA: privacyIDEAAdapter, Sessions: sessions, Emitter: emitter, } mux.Handle("/authorize", enforcement.Middleware(authorizeHandler)) mux.Handle("/authorize/callback", authorizeHandler) // Token handler (with enforcement middleware). tokenHandler := &oidc.TokenHandler{ ClientConfig: clients, Sessions: sessions, Users: lldapAdapter, SigningKey: privateKey, Issuer: issuer, TokenLifetime: tokenLifetime, Emitter: emitter, } mux.Handle("/token", enforcement.Middleware(tokenHandler)) // Userinfo handler. mux.Handle("/userinfo", &oidc.UserinfoHandler{ Users: lldapAdapter, SigningKey: &privateKey.PublicKey, Issuer: issuer, Emitter: emitter, }) // Healthz. mux.HandleFunc("/healthz", func(w http.ResponseWriter, _ *http.Request) { w.Header().Set("Content-Type", "application/json") w.WriteHeader(http.StatusOK) _ = json.NewEncoder(w).Encode(map[string]string{ "status": "ok", "version": version, }) }) // Inject emitter into request context. handler := withEmitter(mux, emitter) // ----------------------------------------------------------------- // 13. Start HTTP server. // ----------------------------------------------------------------- addr := fmt.Sprintf(":%d", cfg.Port) if cfg.Port == 0 { addr = ":8080" } log.Info(). Str("issuer", issuer). Str("addr", addr). Str("environment", cfg.Environment). Str("version", version). Msg("starting keycape server") srv := &http.Server{ Addr: addr, Handler: handler, ReadTimeout: 30 * time.Second, WriteTimeout: 30 * time.Second, IdleTimeout: 120 * time.Second, } if err := srv.ListenAndServe(); err != nil && err != http.ErrServerClosed { log.Error().Err(err).Msg("server error") os.Exit(1) } } // --------------------------------------------------------------------------- // Helpers // --------------------------------------------------------------------------- // loadPrivateKey reads a PEM file and parses the RSA private key. // Supports both PKCS#1 ("RSA PRIVATE KEY") and PKCS#8 ("PRIVATE KEY") PEM blocks. func loadPrivateKey(path string) (*rsa.PrivateKey, error) { data, err := os.ReadFile(path) if err != nil { return nil, fmt.Errorf("read key file: %w", err) } block, _ := pem.Decode(data) if block == nil { return nil, fmt.Errorf("no PEM block found in %q", path) } switch block.Type { case "RSA PRIVATE KEY": key, err := x509.ParsePKCS1PrivateKey(block.Bytes) if err != nil { return nil, fmt.Errorf("parse PKCS1 private key: %w", err) } return key, nil case "PRIVATE KEY": key, err := x509.ParsePKCS8PrivateKey(block.Bytes) if err != nil { return nil, fmt.Errorf("parse PKCS8 private key: %w", err) } rsaKey, ok := key.(*rsa.PrivateKey) if !ok { return nil, fmt.Errorf("private key is not an RSA key") } return rsaKey, nil default: return nil, fmt.Errorf("unexpected PEM block type %q; expected RSA PRIVATE KEY or PRIVATE KEY", block.Type) } } // buildClientRegistry converts []ClientConfig into the map used by handlers. func buildClientRegistry(cfgClients []config.ClientConfig) (map[string]*domain.Client, error) { m := make(map[string]*domain.Client, len(cfgClients)) for i := range cfgClients { c := &cfgClients[i] clientSecret := "" if strings.HasPrefix(c.SecretRef, "env:") { envName := strings.TrimPrefix(c.SecretRef, "env:") clientSecret = os.Getenv(envName) if clientSecret == "" { return nil, fmt.Errorf("client %q secret environment variable %q is empty", c.ClientID, envName) } } m[c.ClientID] = &domain.Client{ ClientID: c.ClientID, DisplayName: c.DisplayName, RedirectURIs: c.RedirectURIs, AllowedScopes: c.AllowedScopes, GrantTypes: c.GrantTypes, ClientType: c.ClientType, SecretRef: c.SecretRef, ClientSecret: clientSecret, ServiceSubject: c.ServiceSubject, Tenant: c.Tenant, Roles: c.Roles, MFARequired: c.MFARequired, } } return m, nil } // withEmitter wraps a handler to inject the telemetry emitter into every request context. func withEmitter(next http.Handler, e telemetry.Emitter) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { ctx := telemetry.WithEmitter(context.Background(), e) next.ServeHTTP(w, r.WithContext(ctx)) }) }