diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 226831ce..d23bfb18 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -30,10 +30,10 @@ Building Tornjak manually can be done with the Makefile. Notable make targets fo - `make bin/tornjak-backend`: makes the Go executable of the Tornjak backend - `make bin/tornjak-manager`: makes the Go executable of the Tornjak manager - `make frontend-local-build`: makes the optimized ReactJS app locally for the Tornjak frontend. Uses environment variable configuration as in tornjak-frontend/.env -- `make container-backend`: containerizes Go executable of the Tornjak backend -- `make container-manager`:containerizes Go executable of the Tornjak manager -- `make container-frontend`: containerizes React JS app for the Tornjak frontend -- `make container-tornjak`: containerizes Tornjak backend with Tornjak frontend +- `make image-tornjak-backend`: containerizes Go executable of the Tornjak backend +- `make image-tornjak-manager`:containerizes Go executable of the Tornjak manager +- `make image-tornjak-frontend`: containerizes React JS app for the Tornjak frontend +- `make image-tornjak`: containerizes Tornjak backend with Tornjak frontend For usage instructions of the containers, please see our [USAGE document](./USAGE.md) to get started. diff --git a/USAGE.md b/USAGE.md index 34bdd971..c870613e 100644 --- a/USAGE.md +++ b/USAGE.md @@ -17,6 +17,7 @@ Please see below for compatibility charts of SPIRE server versions with Tornjak: | Tornjak version | SPIRE Server version | | :--------------------- | :------------------- | | v1.1.x, v1.2.x, v1.3.x | v1.1.x, v1.2.x, v1.3.x, v1.4.x | +| v1.4.x | v1.5.x, v1.6.x, v1.7.x | ## Tornjak Backend diff --git a/docs/conf/agent/base.conf b/docs/conf/agent/base.conf index 586d41b5..6edb3ae5 100644 --- a/docs/conf/agent/base.conf +++ b/docs/conf/agent/base.conf @@ -5,7 +5,7 @@ server { # [required] configure HTTP connection to Tornjak server http { - port = 10080 # opens at port 10080 + port = 10000 # opens at port 10000 } } diff --git a/docs/conf/agent/full.conf b/docs/conf/agent/full.conf index c8d77799..a296f78c 100644 --- a/docs/conf/agent/full.conf +++ b/docs/conf/agent/full.conf @@ -10,7 +10,7 @@ server { # [required] configure HTTP connection to Tornjak server http { - port = 10080 # container port for HTTP connection + port = 10000 # container port for HTTP connection } # [optional, recommended] configure HTTPS connection to Tornjak server @@ -18,7 +18,7 @@ server { port = 10443 # [required for HTTPS] container port for HTTPS connection cert = "sample-keys/tls.pem" # [required for HTTPS] TLS cert key = "sample-keys/key.pem" # [required for HTTPS] TLS key - ca = "sample-keys/rootCA.pem" # enables mTLS connection for HTTPS port + client_ca = "sample-keys/rootCA.pem" # enables mTLS connection for HTTPS port } ### END SERVER CONNECTION CONFIGURATION ### diff --git a/docs/config-tornjak-server.md b/docs/config-tornjak-server.md index c534f801..2a12a071 100644 --- a/docs/config-tornjak-server.md +++ b/docs/config-tornjak-server.md @@ -34,7 +34,7 @@ Runs the tornjak server. The Tornjak config that is passed in must follow a specific format. Examples of this format can be found [below](#sample-configuration-files). In general, it is split into the `server` section with [general Tornjak server configs](#general-tornjak-server-configs), and the `plugins` section. ## General Tornjak Server Configs -The server config will contain information for the three potential connections: HTTP, TLS, and mTLS. See below for sample configuration: +The server config will contain information for the two potential connections: HTTP and HTTPS. HTTPS can be configured to follow TLS or mTLS protocol. See below for sample configuration: ```hcl server { @@ -42,22 +42,22 @@ server { spire_socket_path = "unix:///tmp/spire-server/private/api.sock" # socket to communicate with SPIRE server http { # required block - port = 10080 # if HTTP enabled, opens HTTP listen port at container port 10080 + port = 10000 # if HTTP enabled, opens HTTP listen port at container port 10000 } - https { + https { # optional, recommended block port = 10443 # if enabled, opens HTTPS listen port at container port 10443 cert = "sample-keys/tls.pem" # path of certificate for TLS key = "sample-keys/key.pem" # path of keys for TLS - ca = "sample-keys/userCA.pem" # [optional, enables mTLS] User CA + client_ca = "sample-keys/userCA.pem" # [optional, enables mTLS] User CA } } ``` -We have three connection types that can be opened by the server simultaneously: HTTP, TLS, and mTLS. At least one must be enabled, or the program will exit immediately. If one connection crashes, the error is logged, and the others will still run. When all crash, the Tornjak server exits and the container terminates. +We have two connection types that are opened by the server simultaneously: HTTP and HTTPS. HTTP is always operational. The optional HTTPS connection is recommended for production use case. When HTTPS is configured, the HTTP connection will redirect to the HTTPS (port and service). -If a specific section is omitted or not enabled, that connection will not be created. If all are omitted or disabled, the program will exit immediately with an appropriate error log. +Under the HTTPS block, the fields `port`, `cert`, and `key` are required to enable TLS connection. To enable the mutual TLS (mTLS), you must additionally include the `client_ca` field, so the verification can be done bi-directionally. For examples on enabling TLS and mTLS connections, please see [our TLS and mTLS documentation](../sample-keys/README.md). diff --git a/examples/tls_mtls/README.md b/examples/tls_mtls/README.md index 1f0bfeea..2b597a35 100644 --- a/examples/tls_mtls/README.md +++ b/examples/tls_mtls/README.md @@ -160,7 +160,7 @@ Now that we have mounted the relevant files in step 1, we must configure the Tor More details on the configmap can be found [in our config documentation](../../docs/config-tornjak-server.md). -One Tornjak server can open three connections simultaneously: HTTP, TLS, and mTLS, and at least one must be enabled. A configuration that opens all three is provided in [the configmap in this directory](./tornjak-configmap.yaml) +One Tornjak server opens two connections simultaneously: HTTP and HTTPS. HTTP is always enabled, and when HTTPS is enabled, the HTTP port reroutes to the HTTPS port. A configuration that enables TLS is provided in [the configmap](./tornjak-configmap.yaml). To learn about the configurations each of the TLS and mTLS, expand below sections. @@ -171,17 +171,16 @@ The TLS configuration requires the port number on which to open the connection a ``` server { ... - tls { - enabled = true - port = 20000 # container port for TLS connection - cert = "server/tls.crt" # TLS cert + https { + port = 10443 # container port for TLS connection + cert = "server/tls.crt" # TLS cert key = "server/tls.key" # TLS key } ... } ``` -In the above configuration, we create TLS connection at `localhost:20000` that uses certificate key pair at paths `server/tls.cert` and `server/tls.key` respectively. An example of the TLS configuration is found in the current directory at `tornjak-configmap.yaml`. +In the above configuration, we create TLS connection at `localhost:10443` that uses certificate key pair at paths `server/tls.cert` and `server/tls.key` respectively. An example of the TLS configuration is found in the current directory at `tornjak-configmap.yaml`. A call to this port will require a CA that can verify the `cert/key` pair given. We can see that making a curl command to this port will create an error. First port-forward this connection to `localhost:20000` @@ -194,18 +193,17 @@ The mTLS configuration, much like the TLS configuration, requires the port numbe ``` server { ... - mtls { - enabled = true - port = 30000 # container port for mTLS connection - cert = "server/tls.crt" # mTLS cert - key = "server/tls.key" # mTLS key - ca = "users/userCA.crt" # mTLS CA + https { + port = 10443 # container port for mTLS connection + cert = "server/tls.crt" # TLS cert + key = "server/tls.key" # TLS key + ca = "users/userCA.crt" # user CA for mTLS [Removing this line creates a TLS connection] } ... } ``` -The above configuration enables mTLS at `localhost:30000` that uses certificate/key pair at paths `server/tls.crt` and `server/tls.key` respectively. It verifies caller certificate/key pairs with ca certificate at path `server/CA/rootCA.pem`. An example of the TLS configuration is found in the current directory at `tornjak-configmap.yaml`. +The above configuration enables mTLS at `localhost:10443` that uses certificate/key pair at paths `server/tls.crt` and `server/tls.key` respectively. It verifies caller certificate/key pairs with ca certificate at path `server/CA/rootCA.pem`. An example of the TLS configuration is found in the current directory at `tornjak-configmap.yaml`. A call to this port requires a CA that can verify the `cert/key` pair given, as well as a cert/key pair signed by the CA with the `ca` certificate. @@ -224,16 +222,16 @@ Now if we take a look at the logs, you can see the relevant connections have bee kubectl logs -n spire spire-server-0 -c tornjak-backend ``` -If we try to open the service to `localhost:20000`: +If we try to open the service to `localhost:10443`: ``` -kubectl -n spire port-forward spire-server-0 20000:20000 +kubectl -n spire port-forward spire-server-0 10443:10443 ``` Then attempt curl command: ``` -curl https://localhost:20000 +curl https://localhost:10443 ``` ``` @@ -255,6 +253,19 @@ We will show how to make a proper curl command in the next section. Now that we have opened TLS and mTLS connection, we may make calls. You will need the url to access the endpoints. If you have deployed locally on Minikube, as in the quickstart, you will need to port-forward the container to localhost. +### Redirect from HTTP Port + +Notice that with this new configuration, a `curl` command to the HTTP port returns a permanent redirect to the URL of the HTTPS port: + +``` +curl http:// +``` + +``` +Found. +``` + + ### Make a TLS call In order to make a TLS call we need only a CA certificate that can validate the certificate/key pair given to Tornjak in step 1. In our case, we can use the certificate within `CA-server`: diff --git a/examples/tls_mtls/tornjak-configmap.yaml b/examples/tls_mtls/tornjak-configmap.yaml index c99f4091..b7b3053a 100644 --- a/examples/tls_mtls/tornjak-configmap.yaml +++ b/examples/tls_mtls/tornjak-configmap.yaml @@ -13,23 +13,14 @@ data: # configure HTTP connection to Tornjak server http { - enabled = true port = 10000 # opens at port 10000 } - tls { - enabled = true - port = 20000 + https { + port = 10443 cert = "server/tls.crt" key = "server/tls.key" - } - - mtls { - enabled = true - port = 30000 - cert = "server/tls.crt" - key = "server/tls.key" - ca = "users/rootCA.crt" + # client_ca = "users/rootCA.crt" } } diff --git a/tornjak-backend/api/agent/server.go b/tornjak-backend/api/agent/server.go index 16d3802f..8c29dd81 100644 --- a/tornjak-backend/api/agent/server.go +++ b/tornjak-backend/api/agent/server.go @@ -11,6 +11,7 @@ import ( "path/filepath" "strings" "time" + "crypto/tls" "github.com/cenkalti/backoff/v4" "github.com/gorilla/mux" @@ -582,21 +583,22 @@ func (s *Server) GetRouter() http.Handler { return rtr } -func redirectHTTP(w http.ResponseWriter, r *http.Request) { +func (s *Server) redirectHTTP(w http.ResponseWriter, r *http.Request) { if r.Method != "GET" && r.Method != "HEAD" { http.Error(w, "Use HTTPS", http.StatusBadRequest) return } - target := "https://" + stripPort(r.Host) + r.URL.RequestURI() + target := "https://" + s.stripPort(r.Host) + r.URL.RequestURI() http.Redirect(w, r, target, http.StatusFound) } -func stripPort(hostport string) string { +func (s *Server) stripPort(hostport string) string { host, _, err := net.SplitHostPort(hostport) if err != nil { return hostport } - return net.JoinHostPort(host, "443") + addr := fmt.Sprintf("%d", s.TornjakConfig.Server.HTTPSConfig.ListenPort) + return net.JoinHostPort(host, addr) } // HandleRequests connects api links with respective functions @@ -607,22 +609,67 @@ func (s *Server) HandleRequests() { log.Fatal("Cannot Configure: ", err) } - numPorts := 0 // TODO: replace with workerGroup for thread safety + // TODO: replace with workerGroup for thread safety errChannel := make(chan error, 2) - var httpHandler http.Handler = http.HandlerFunc(redirectHTTP) + serverConfig := s.TornjakConfig.Server if serverConfig.HTTPConfig == nil { - serverConfig.HTTPConfig = &HTTPConfig{ - ListenPort: 80, - } + err = fmt.Errorf("HTTP Config error: no port configured") + errChannel <- err + return } - if serverConfig.HTTPSConfig == nil { - httpHandler = s.GetRouter() + // default router does not redirect + httpHandler := s.GetRouter() + numPorts := 1 + + if serverConfig.HTTPSConfig == nil { // warn when HTTPS not configured log.Print("WARNING: Please consider configuring HTTPS to ensure traffic is running on encrypted endpoint!") + } else { + numPorts += 1 + + httpHandler = http.HandlerFunc(s.redirectHTTP) + canStartHTTPS := true + httpsConfig := serverConfig.HTTPSConfig + var tlsConfig *tls.Config + + + if serverConfig.HTTPSConfig.ListenPort == 0 { + // Fail because this is required field in this section + err = fmt.Errorf("HTTPS Config error: no port configured. Starting insecure HTTP connection at %d...", serverConfig.HTTPConfig.ListenPort) + errChannel <- err + httpHandler = s.GetRouter() + canStartHTTPS = false + } else { + tlsConfig, err = httpsConfig.Parse() + if err != nil { + err = fmt.Errorf("failed parsing HTTPS config: %w. Starting insecure HTTP connection at %d...", err, serverConfig.HTTPConfig.ListenPort) + errChannel <- err + httpHandler = s.GetRouter() + canStartHTTPS = false + } + } + + if canStartHTTPS { + go func() { + addr := fmt.Sprintf(":%d", serverConfig.HTTPSConfig.ListenPort) + // Create a Server instance to listen on port 8443 with the TLS config + server := &http.Server{ + Handler: s.GetRouter(), + Addr: addr, + TLSConfig: tlsConfig, + } + + fmt.Printf("Starting https on %s...\n", addr) + err = server.ListenAndServeTLS(httpsConfig.Cert, httpsConfig.Key) + if err != nil { + err = fmt.Errorf("server error serving on https: %w", err) + errChannel <- err + } + }() + } } - numPorts = 1 go func() { addr := fmt.Sprintf(":%d", serverConfig.HTTPConfig.ListenPort) fmt.Printf("Starting to listen on %s...\n", addr) @@ -632,37 +679,6 @@ func (s *Server) HandleRequests() { } }() - if serverConfig.HTTPSConfig != nil { - numPorts += 1 - go func() { - if serverConfig.HTTPSConfig.ListenPort == 0 { - serverConfig.HTTPSConfig.ListenPort = 443 - } - tls := serverConfig.HTTPSConfig.TLS - tlsConfig, err := tls.Parse() - if err != nil { - err = fmt.Errorf("failed parsing tls: %w", err) - errChannel <- err - return - } - - addr := fmt.Sprintf(":%d", serverConfig.HTTPSConfig.ListenPort) - // Create a Server instance to listen on port 8443 with the TLS config - server := &http.Server{ - Handler: s.GetRouter(), - Addr: addr, - TLSConfig: tlsConfig, - } - - fmt.Printf("Starting https on %s...\n", addr) - err = server.ListenAndServeTLS(tls.Cert, tls.Key) - if err != nil { - err = fmt.Errorf("server error serving on https: %w", err) - errChannel <- err - } - }() - } - // as errors come in, read them, and block for i := 0; i < numPorts; i++ { err := <-errChannel diff --git a/tornjak-backend/api/agent/types.go b/tornjak-backend/api/agent/types.go index 39acada2..d4e5aadb 100644 --- a/tornjak-backend/api/agent/types.go +++ b/tornjak-backend/api/agent/types.go @@ -39,25 +39,23 @@ type HTTPConfig struct { type HTTPSConfig struct { ListenPort int `hcl:"port"` - TLS TLSConfig `hcl:"tls"` + Cert string `hcl:"cert"` + Key string `hcl:"key"` + ClientCA string `hcl:"client_ca"` } -type TLSConfig struct { - Cert string `hcl:"cert"` - Key string `hcl:"key"` - ClientCA string `hcl:"client_ca"` -} +func (h HTTPSConfig) Parse() (*tls.Config, error) { + serverCertPath := h.Cert + serverKeyPath := h.Key + clientCAPath := h.ClientCA -func (t TLSConfig) Parse() (*tls.Config, error) { - serverCertPath := t.Cert - serverKeyPath := t.Key - clientCAPath := t.ClientCA + mtls := (clientCAPath != "") if _, err := os.Stat(serverCertPath); os.IsNotExist(err) { - return nil, fmt.Errorf("server cert path: %w", err) + return nil, fmt.Errorf("server cert path '%s': %w", serverCertPath, err) } if _, err := os.Stat(serverKeyPath); os.IsNotExist(err) { - return nil, fmt.Errorf("server key path: %w", err) + return nil, fmt.Errorf("server key path '%s': %w", serverKeyPath, err) } // Create a CA certificate pool and add cert.pem to it @@ -68,20 +66,26 @@ func (t TLSConfig) Parse() (*tls.Config, error) { caCertPool := x509.NewCertPool() caCertPool.AppendCertsFromPEM(serverCert) - // add mTLS CA path to cert pool as well - if _, err := os.Stat(clientCAPath); os.IsNotExist(err) { - return nil, fmt.Errorf("server file does not exist %s", clientCAPath) - } - clientCA, err := os.ReadFile(clientCAPath) - if err != nil { - return nil, fmt.Errorf("server: could not read file %s: %w", clientCAPath, err) + if mtls { + // add mTLS CA path to cert pool as well + if _, err := os.Stat(clientCAPath); os.IsNotExist(err) { + return nil, fmt.Errorf("server file does not exist %s", clientCAPath) + } + clientCA, err := os.ReadFile(clientCAPath) + if err != nil { + return nil, fmt.Errorf("server: could not read file %s: %w", clientCAPath, err) + } + caCertPool.AppendCertsFromPEM(clientCA) } - caCertPool.AppendCertsFromPEM(clientCA) // Create the TLS Config with the CA pool and enable Client certificate validation tlsConfig := &tls.Config{ ClientCAs: caCertPool, } + + if mtls { + tlsConfig.ClientAuth = tls.RequireAndVerifyClientCert + } //tlsConfig.BuildNameToCertificate() return tlsConfig, nil