From ed5721d39ec5a1559cbfbac65857a489fc59265b Mon Sep 17 00:00:00 2001 From: Livio Spring Date: Fri, 24 Jun 2022 14:38:22 +0200 Subject: [PATCH] feat: TLS support (#3862) * feat: TLS support * add comment * fix comment --- cmd/admin/setup/setup.go | 5 +++ cmd/admin/start/config.go | 2 + cmd/admin/start/flags.go | 5 ++- cmd/admin/start/start.go | 36 ++++++++++++---- cmd/admin/start/start_from_init.go | 4 ++ cmd/admin/tls/tls.go | 46 ++++++++++++++++++++ cmd/defaults.yaml | 27 ++++++++++++ docs/docs/guides/installation/linux.mdx | 3 +- docs/docs/guides/installation/macos.mdx | 3 +- internal/api/api.go | 5 ++- internal/api/grpc/server/server.go | 13 ++++-- internal/config/network/config.go | 56 +++++++++++++++++++++++++ 12 files changed, 186 insertions(+), 19 deletions(-) create mode 100644 cmd/admin/tls/tls.go create mode 100644 internal/config/network/config.go diff --git a/cmd/admin/setup/setup.go b/cmd/admin/setup/setup.go index 80721c39ef..578127fe81 100644 --- a/cmd/admin/setup/setup.go +++ b/cmd/admin/setup/setup.go @@ -9,6 +9,7 @@ import ( "github.com/zitadel/logging" "github.com/zitadel/zitadel/cmd/admin/key" + "github.com/zitadel/zitadel/cmd/admin/tls" "github.com/zitadel/zitadel/internal/database" "github.com/zitadel/zitadel/internal/eventstore" "github.com/zitadel/zitadel/internal/migration" @@ -28,6 +29,9 @@ func New() *cobra.Command { Requirements: - cockroachdb`, Run: func(cmd *cobra.Command, args []string) { + err := tls.ModeFromFlag(cmd) + logging.OnError(err).Fatal("invalid tlsMode") + config := MustNewConfig(viper.GetViper()) steps := MustNewSteps(viper.New()) @@ -46,6 +50,7 @@ Requirements: func Flags(cmd *cobra.Command) { cmd.PersistentFlags().StringArrayVar(&stepFiles, "steps", nil, "paths to step files to overwrite default steps") key.AddMasterKeyFlag(cmd) + tls.AddTLSModeFlag(cmd) } func Setup(config *Config, steps *Steps, masterKey string) { diff --git a/cmd/admin/start/config.go b/cmd/admin/start/config.go index 42ed801ebb..179ab3ed4a 100644 --- a/cmd/admin/start/config.go +++ b/cmd/admin/start/config.go @@ -16,6 +16,7 @@ import ( auth_es "github.com/zitadel/zitadel/internal/auth/repository/eventsourcing" "github.com/zitadel/zitadel/internal/command" "github.com/zitadel/zitadel/internal/config/hook" + "github.com/zitadel/zitadel/internal/config/network" "github.com/zitadel/zitadel/internal/config/systemdefaults" "github.com/zitadel/zitadel/internal/crypto" "github.com/zitadel/zitadel/internal/database" @@ -31,6 +32,7 @@ type Config struct { ExternalPort uint16 ExternalDomain string ExternalSecure bool + TLS network.TLS HTTP2HostHeader string HTTP1HostHeader string WebAuthNName string diff --git a/cmd/admin/start/flags.go b/cmd/admin/start/flags.go index c50a9c1edc..f3ca8afe36 100644 --- a/cmd/admin/start/flags.go +++ b/cmd/admin/start/flags.go @@ -5,14 +5,17 @@ import ( "github.com/spf13/viper" "github.com/zitadel/zitadel/cmd/admin/key" + "github.com/zitadel/zitadel/cmd/admin/tls" ) +var tlsMode *string + func startFlags(cmd *cobra.Command) { bindUint16Flag(cmd, "port", "port to run ZITADEL on") bindStringFlag(cmd, "externalDomain", "domain ZITADEL will be exposed on") bindStringFlag(cmd, "externalPort", "port ZITADEL will be exposed on") - bindBoolFlag(cmd, "externalSecure", "if ZITADEL will be served on HTTPS") + tls.AddTLSModeFlag(cmd) key.AddMasterKeyFlag(cmd) } diff --git a/cmd/admin/start/start.go b/cmd/admin/start/start.go index 878d257981..db86780084 100644 --- a/cmd/admin/start/start.go +++ b/cmd/admin/start/start.go @@ -2,6 +2,7 @@ package start import ( "context" + "crypto/tls" "database/sql" _ "embed" "fmt" @@ -21,6 +22,7 @@ import ( "golang.org/x/net/http2/h2c" "github.com/zitadel/zitadel/cmd/admin/key" + cmd_tls "github.com/zitadel/zitadel/cmd/admin/tls" admin_es "github.com/zitadel/zitadel/internal/admin/repository/eventsourcing" "github.com/zitadel/zitadel/internal/api" "github.com/zitadel/zitadel/internal/api/assets" @@ -57,6 +59,10 @@ func New() *cobra.Command { Requirements: - cockroachdb`, RunE: func(cmd *cobra.Command, args []string) error { + err := cmd_tls.ModeFromFlag(cmd) + if err != nil { + return err + } config := MustNewConfig(viper.GetViper()) masterKey, err := key.MasterKey(cmd) if err != nil { @@ -136,14 +142,18 @@ func startZitadel(config *Config, masterKey string) error { notification.Start(config.Notification, config.ExternalPort, config.ExternalSecure, commands, queries, dbClient, assets.HandlerPrefix, config.SystemDefaults.Notifications.FileSystemPath, keys.User, keys.SMTP, keys.SMS) router := mux.NewRouter() - err = startAPIs(ctx, router, commands, queries, eventstoreClient, dbClient, config, storage, authZRepo, keys, config.SystemAPIUsers) + tlsConfig, err := config.TLS.Config() if err != nil { return err } - return listen(ctx, router, config.Port) + err = startAPIs(ctx, router, commands, queries, eventstoreClient, dbClient, config, storage, authZRepo, keys) + if err != nil { + return err + } + return listen(ctx, router, config.Port, tlsConfig) } -func startAPIs(ctx context.Context, router *mux.Router, commands *command.Commands, queries *query.Queries, eventstore *eventstore.Eventstore, dbClient *sql.DB, config *Config, store static.Storage, authZRepo authz_repo.Repository, keys *encryptionKeys, systemAPIKeys map[string]*internal_authz.SystemAPIUser) error { +func startAPIs(ctx context.Context, router *mux.Router, commands *command.Commands, queries *query.Queries, eventstore *eventstore.Eventstore, dbClient *sql.DB, config *Config, store static.Storage, authZRepo authz_repo.Repository, keys *encryptionKeys) error { repo := struct { authz_repo.Repository *query.Queries @@ -151,9 +161,12 @@ func startAPIs(ctx context.Context, router *mux.Router, commands *command.Comman authZRepo, queries, } - verifier := internal_authz.Start(repo, http_util.BuildHTTP(config.ExternalDomain, config.ExternalPort, config.ExternalSecure), systemAPIKeys) - - apis := api.New(config.Port, router, queries, verifier, config.InternalAuthZ, config.ExternalSecure, config.HTTP2HostHeader, config.HTTP1HostHeader) + verifier := internal_authz.Start(repo, http_util.BuildHTTP(config.ExternalDomain, config.ExternalPort, config.ExternalSecure), config.SystemAPIUsers) + tlsConfig, err := config.TLS.Config() + if err != nil { + return err + } + apis := api.New(config.Port, router, queries, verifier, config.InternalAuthZ, config.ExternalSecure, tlsConfig, config.HTTP2HostHeader, config.HTTP1HostHeader) authRepo, err := auth_es.Start(config.Auth, config.SystemDefaults, commands, queries, dbClient, keys.OIDC, keys.User) if err != nil { return fmt.Errorf("error starting auth repo: %w", err) @@ -215,9 +228,9 @@ func startAPIs(ctx context.Context, router *mux.Router, commands *command.Comman return nil } -func listen(ctx context.Context, router *mux.Router, port uint16) error { +func listen(ctx context.Context, router *mux.Router, port uint16, tlsConfig *tls.Config) error { http2Server := &http2.Server{} - http1Server := &http.Server{Handler: h2c.NewHandler(router, http2Server)} + http1Server := &http.Server{Handler: h2c.NewHandler(router, http2Server), TLSConfig: tlsConfig} lis, err := net.Listen("tcp", fmt.Sprintf(":%d", port)) if err != nil { return fmt.Errorf("tcp listener on %d failed: %w", port, err) @@ -227,7 +240,12 @@ func listen(ctx context.Context, router *mux.Router, port uint16) error { go func() { logging.Infof("server is listening on %s", lis.Addr().String()) - errCh <- http1Server.Serve(lis) + if tlsConfig != nil { + //we don't need to pass the files here, because we already initialized the TLS config on the server + errCh <- http1Server.ServeTLS(lis, "", "") + } else { + errCh <- http1Server.Serve(lis) + } }() shutdown := make(chan os.Signal, 1) diff --git a/cmd/admin/start/start_from_init.go b/cmd/admin/start/start_from_init.go index 85fc4d2c65..f6057551c1 100644 --- a/cmd/admin/start/start_from_init.go +++ b/cmd/admin/start/start_from_init.go @@ -8,6 +8,7 @@ import ( "github.com/zitadel/zitadel/cmd/admin/initialise" "github.com/zitadel/zitadel/cmd/admin/key" "github.com/zitadel/zitadel/cmd/admin/setup" + "github.com/zitadel/zitadel/cmd/admin/tls" ) func NewStartFromInit() *cobra.Command { @@ -22,6 +23,9 @@ Last ZITADEL starts. Requirements: - cockroachdb`, Run: func(cmd *cobra.Command, args []string) { + err := tls.ModeFromFlag(cmd) + logging.OnError(err).Fatal("invalid tlsMode") + masterKey, err := key.MasterKey(cmd) logging.OnError(err).Panic("No master key provided") diff --git a/cmd/admin/tls/tls.go b/cmd/admin/tls/tls.go new file mode 100644 index 0000000000..e9c5249410 --- /dev/null +++ b/cmd/admin/tls/tls.go @@ -0,0 +1,46 @@ +package tls + +import ( + "errors" + + "github.com/spf13/cobra" + "github.com/spf13/viper" +) + +const ( + flagTLSMode = "tlsMode" +) + +var ( + ErrValidValue = errors.New("value must either be `enabled`, `external` or `disabled`") +) + +func AddTLSModeFlag(cmd *cobra.Command) { + if cmd.PersistentFlags().Lookup(flagTLSMode) != nil { + return + } + cmd.PersistentFlags().String(flagTLSMode, "", "start ZITADEL with (enabled), without (disabled) TLS or external component e.g. reverse proxy (external) terminating TLS, this flag will overwrite `externalSecure` and `tls.enabled` in configs files") +} + +func ModeFromFlag(cmd *cobra.Command) error { + tlsMode, _ := cmd.Flags().GetString(flagTLSMode) + var tlsEnabled, externalSecure bool + switch tlsMode { + case "enabled": + tlsEnabled = true + externalSecure = true + case "external": + tlsEnabled = false + externalSecure = true + case "disabled": + tlsEnabled = false + externalSecure = false + case "": + return nil + default: + return ErrValidValue + } + viper.Set("tls.enabled", tlsEnabled) + viper.Set("externalSecure", externalSecure) + return nil +} diff --git a/cmd/defaults.yaml b/cmd/defaults.yaml index de1a51496e..8f4cc9d6d0 100644 --- a/cmd/defaults.yaml +++ b/cmd/defaults.yaml @@ -3,11 +3,38 @@ Log: Formatter: Format: text +# Port ZITADEL will listen on Port: 8080 +# Port ZITADEL is exposed on, it can differ from port e.g. if you proxy the traffic +# !!! Changing this after initial setup breaks your system !!! ExternalPort: 8080 +# Domain / hostname ZITADEL is exposed externally +# !!! Changing this after initial setup breaks your system !!! ExternalDomain: localhost +# specifies if ZITADEL is exposed externally through TLS +# this must be set to true even if TLS is not enabled on ZITADEL itself +# but TLS traffic is terminated on a reverse proxy +# !!! Changing this after initial setup breaks your system !!! ExternalSecure: true +TLS: + # if enabled, ZITADEL will serve all traffic over TLS (HTTPS and gRPC) + # you must then also provide a private key and certificate to be used for the connection + # either directly or by a path to the corresponding file + Enabled: true + # Path to the private key of the TLS certificate, it will be loaded into the Key + # and overwrite any exising value + KeyPath: #/path/to/key/file.pem + # Private key of the TLS certificate (KeyPath will this overwrite, if specified) + Key: # + # Path to the certificate for the TLS connection, it will be loaded into the Cert + # and overwrite any exising value + CertPath: #/path/to/cert/file.pem + # Certificate for the TLS connection (CertPath will this overwrite, if specified) + Cert: # + +# Header name of HTTP2 (incl. gRPC) calls from which the instance will be matched HTTP2HostHeader: ":authority" +# Header name of HTTP1 calls from which the instance will be matched HTTP1HostHeader: "host" WebAuthNName: ZITADEL diff --git a/docs/docs/guides/installation/linux.mdx b/docs/docs/guides/installation/linux.mdx index dd8c39278b..62594bad82 100644 --- a/docs/docs/guides/installation/linux.mdx +++ b/docs/docs/guides/installation/linux.mdx @@ -14,7 +14,6 @@ cockroach start-single-node --insecure --background --http-addr :9090 Configure your environment and generate a master encryption key ```bash -export ZITADEL_EXTERNALSECURE=false export ZITADEL_EXTERNALDOMAIN=localhost export ZITADEL_DEFAULTINSTANCE_CUSTOMDOMAIN=localhost ``` @@ -28,5 +27,5 @@ curl -s https://api.github.com/repos/zitadel/zitadel/releases/tags/v2.0.0-v2-alp Run the database and application containers ```bash -zitadel admin start-from-init --masterkey "MasterkeyNeedsToHave32Characters" +zitadel admin start-from-init --tlsMode disabled --masterkey "MasterkeyNeedsToHave32Characters" ``` diff --git a/docs/docs/guides/installation/macos.mdx b/docs/docs/guides/installation/macos.mdx index 549a1050c9..800e4c2891 100644 --- a/docs/docs/guides/installation/macos.mdx +++ b/docs/docs/guides/installation/macos.mdx @@ -14,7 +14,6 @@ cockroach start-single-node --insecure --background --http-addr :9090 Configure your environment and generate a master encryption key ```bash -export ZITADEL_EXTERNALSECURE=false export ZITADEL_EXTERNALDOMAIN=localhost export ZITADEL_DEFAULTINSTANCE_CUSTOMDOMAIN=localhost export MY_ARCHITECTURE="arm64 or amd64 depeding on your mac" @@ -37,5 +36,5 @@ curl -s https://api.github.com/repos/zitadel/zitadel/releases/tags/v2.0.0-v2-alp Run ZITADEL ```bash -zitadel admin start-from-init --masterkey "MasterkeyNeedsToHave32Characters" +zitadel admin start-from-init --tlsMode disabled --masterkey "MasterkeyNeedsToHave32Characters" ``` diff --git a/internal/api/api.go b/internal/api/api.go index b4d5c6eb72..d2bc29f106 100644 --- a/internal/api/api.go +++ b/internal/api/api.go @@ -2,6 +2,7 @@ package api import ( "context" + "crypto/tls" "net/http" "strings" @@ -34,7 +35,7 @@ type health interface { Instance(ctx context.Context, shouldTriggerBulk bool) (*query.Instance, error) } -func New(port uint16, router *mux.Router, queries *query.Queries, verifier *internal_authz.TokenVerifier, authZ internal_authz.Config, externalSecure bool, http2HostName, http1HostName string) *API { +func New(port uint16, router *mux.Router, queries *query.Queries, verifier *internal_authz.TokenVerifier, authZ internal_authz.Config, externalSecure bool, tlsConfig *tls.Config, http2HostName, http1HostName string) *API { api := &API{ port: port, verifier: verifier, @@ -43,7 +44,7 @@ func New(port uint16, router *mux.Router, queries *query.Queries, verifier *inte externalSecure: externalSecure, http1HostName: http1HostName, } - api.grpcServer = server.CreateServer(api.verifier, authZ, queries, http2HostName) + api.grpcServer = server.CreateServer(api.verifier, authZ, queries, http2HostName, tlsConfig) api.routeGRPC() api.RegisterHandler("/debug", api.healthHandler()) diff --git a/internal/api/grpc/server/server.go b/internal/api/grpc/server/server.go index 8f7ea7ecd4..9b932393af 100644 --- a/internal/api/grpc/server/server.go +++ b/internal/api/grpc/server/server.go @@ -1,8 +1,11 @@ package server import ( + "crypto/tls" + grpc_middleware "github.com/grpc-ecosystem/go-grpc-middleware" "google.golang.org/grpc" + "google.golang.org/grpc/credentials" "github.com/zitadel/zitadel/internal/api/authz" grpc_api "github.com/zitadel/zitadel/internal/api/grpc" @@ -20,9 +23,9 @@ type Server interface { AuthMethods() authz.MethodMapping } -func CreateServer(verifier *authz.TokenVerifier, authConfig authz.Config, queries *query.Queries, hostHeaderName string) *grpc.Server { +func CreateServer(verifier *authz.TokenVerifier, authConfig authz.Config, queries *query.Queries, hostHeaderName string, tlsConfig *tls.Config) *grpc.Server { metricTypes := []metrics.MetricType{metrics.MetricTypeTotalCount, metrics.MetricTypeRequestCount, metrics.MetricTypeStatusCode} - return grpc.NewServer( + serverOptions := []grpc.ServerOption{ grpc.UnaryInterceptor( grpc_middleware.ChainUnaryServer( middleware.DefaultTracingServer(), @@ -37,5 +40,9 @@ func CreateServer(verifier *authz.TokenVerifier, authConfig authz.Config, querie middleware.ServiceHandler(), ), ), - ) + } + if tlsConfig != nil { + serverOptions = append(serverOptions, grpc.Creds(credentials.NewTLS(tlsConfig))) + } + return grpc.NewServer(serverOptions...) } diff --git a/internal/config/network/config.go b/internal/config/network/config.go new file mode 100644 index 0000000000..79a374f7d7 --- /dev/null +++ b/internal/config/network/config.go @@ -0,0 +1,56 @@ +package network + +import ( + "crypto/tls" + "errors" + "os" +) + +var ( + ErrMissingConfig = errors.New("") +) + +type TLS struct { + //If enabled, ZITADEL will serve all traffic over TLS (HTTPS and gRPC) + //you must then also provide a private key and certificate to be used for the connection + //either directly or by a path to the corresponding file + Enabled bool + //Path to the private key of the TLS certificate, it will be loaded into the Key + //and overwrite any exising value + KeyPath string + //Path to the certificate for the TLS connection, it will be loaded into the Cert + //and overwrite any exising value + CertPath string + //Private key of the TLS certificate (KeyPath will this overwrite, if specified) + Key []byte + //Certificate for the TLS connection (CertPath will this overwrite, if specified) + Cert []byte +} + +func (t *TLS) Config() (_ *tls.Config, err error) { + if !t.Enabled { + return nil, nil + } + if t.KeyPath != "" { + t.Key, err = os.ReadFile(t.KeyPath) + if err != nil { + return nil, err + } + } + if t.CertPath != "" { + t.Cert, err = os.ReadFile(t.CertPath) + if err != nil { + return nil, err + } + } + if t.Key == nil || t.Cert == nil { + return nil, ErrMissingConfig + } + tlsCert, err := tls.X509KeyPair(t.Cert, t.Key) + if err != nil { + return nil, err + } + return &tls.Config{ + Certificates: []tls.Certificate{tlsCert}, + }, nil +}