diff --git a/api/agent/config.go b/api/agent/config.go index 53e0f443..0faafc33 100644 --- a/api/agent/config.go +++ b/api/agent/config.go @@ -14,6 +14,7 @@ import ( "github.com/spiffe/tornjak/pkg/agent/authentication/authenticator" "github.com/spiffe/tornjak/pkg/agent/authorization" agentdb "github.com/spiffe/tornjak/pkg/agent/db" + "github.com/spiffe/tornjak/pkg/agent/spirecrd" ) func stringFromToken(keyToken token.Token) (string, error) { @@ -34,9 +35,17 @@ func stringFromToken(keyToken token.Token) (string, error) { // getPluginConfig returns first plugin configuration func getPluginConfig(plugin *ast.ObjectItem) (string, ast.Node, error) { // extract plugin name and value - pluginName, err := stringFromToken(plugin.Keys[1].Token) - if err != nil { - return "", nil, fmt.Errorf("invalid plugin type name %q: %w", plugin.Keys[1].Token.Text, err) + pluginKeys := plugin.Keys + var pluginName string + var err error + + if len(pluginKeys) > 1 { + pluginName, err = stringFromToken(pluginKeys[1].Token) + if err != nil { + return "", nil, fmt.Errorf("invalid plugin type name %q: %w", pluginKeys[1].Token.Text, err) + } + } else { + pluginName = "" } // extract data var hclPluginConfig hclPluginConfig @@ -85,6 +94,30 @@ func NewAgentsDB(dbPlugin *ast.ObjectItem) (agentdb.AgentDB, error) { } } +// NewCRDManager returns ... +func NewCRDManager(crdPlugin *ast.ObjectItem) (spirecrd.CRDManager, error) { + _, data, _ := getPluginConfig(crdPlugin) + + // check if data is defined + if data == nil { + return "", errors.New("SPIRECRDManager plugin ('config > plugins > SPIRECRDManager > plugin_data') not populated") + } + // decode config to struct + var config pluginControllerManager + if err := hcl.DecodeObject(&config, data); err != nil { + return "", errors.Errorf("Couldn't parse SPIREControllerManager config: %v", err) + } + + fmt.Println("CRD Controller configured. WARNING: This is currently a no-op") + + crdManager, err := spirecrd.NewSPIRECRDManager(config.Classname) + if err != nil { + return nil, errors.Errorf("Could not initialize CRD manager: %v", err) + } + + return crdManager, nil +} + // NewAuthenticator returns a new Authenticator func NewAuthenticator(authenticatorPlugin *ast.ObjectItem) (authenticator.Authenticator, error) { key, data, _ := getPluginConfig(authenticatorPlugin) @@ -225,10 +258,6 @@ func (s *Server) Configure() error { // iterate over plugin list for _, pluginObject := range pluginList.Items { - if len(pluginObject.Keys) != 2 { - return fmt.Errorf("plugin item expected to have two keys (type then name)") - } - pluginType, err := stringFromToken(pluginObject.Keys[0].Token) if err != nil { return fmt.Errorf("invalid plugin type key %q: %w", pluginObject.Keys[0].Token.Text, err) @@ -238,18 +267,37 @@ func (s *Server) Configure() error { switch pluginType { // configure datastore case "DataStore": + if len(pluginObject.Keys) != 2 { + return fmt.Errorf("plugin DataStore expected to have two keys (type then name)") + } s.Db, err = NewAgentsDB(pluginObject) if err != nil { return errors.Errorf("Cannot configure datastore plugin: %v", err) } + // configure controller maanger CRD management + case "SPIRECRDManager": + if len(pluginObject.Keys) != 1 { + return fmt.Errorf("plugin SPIRECRDManager expected to have one key (type)") + } + + s.CRDManager, err = NewCRDManager(pluginObject) + if err != nil { + return errors.Errorf("Cannot configure CRD management plugin: %v", err) + } // configure Authenticator case "Authenticator": + if len(pluginObject.Keys) != 2 { + return fmt.Errorf("plugin Authenticator expected to have two keys (type then name)") + } s.Authenticator, err = NewAuthenticator(pluginObject) if err != nil { return errors.Errorf("Cannot configure Authenticator plugin: %v", err) } // configure Authorizer case "Authorizer": + if len(pluginObject.Keys) != 2 { + return fmt.Errorf("plugin Authorizer expected to have two keys (type then name)") + } s.Authorizer, err = NewAuthorizer(pluginObject) if err != nil { return errors.Errorf("Cannot configure Authorizer plugin: %v", err) diff --git a/api/agent/server.go b/api/agent/server.go index e8410d31..2405d65f 100644 --- a/api/agent/server.go +++ b/api/agent/server.go @@ -18,6 +18,7 @@ import ( "github.com/spiffe/tornjak/pkg/agent/authentication/authenticator" "github.com/spiffe/tornjak/pkg/agent/authorization" agentdb "github.com/spiffe/tornjak/pkg/agent/db" + "github.com/spiffe/tornjak/pkg/agent/spirecrd" ) type Server struct { @@ -32,6 +33,7 @@ type Server struct { // Plugins Db agentdb.AgentDB + CRDManager spirecrd.CRDManager Authenticator authenticator.Authenticator Authorizer authorization.Authorizer } diff --git a/api/agent/types.go b/api/agent/types.go index ce81b402..3ded588e 100644 --- a/api/agent/types.go +++ b/api/agent/types.go @@ -1,132 +1,136 @@ -package api - -import ( - "crypto/tls" - "crypto/x509" - "fmt" - "os" - - "github.com/hashicorp/hcl/hcl/ast" -) - -// TornjakServerInfo provides insight into the configuration of the SPIRE server -// where the Tornjak Agent resides -type TornjakSpireServerInfo struct { - // Plugins is a map from plugin types to respective names of plugins configured - Plugins map[string][]string `json:"plugins"` - // TrustDomain specifies the trust domain of the SPIRE server configured with tornjak - TrustDomain string `json:"trustDomain"` - // Verbose config contains unstructure information on the config on the agent - VerboseConfig string `json:"verboseConfig"` -} - -// pared down version of full Server Config type spire/cmd/spire-server/cli/run -// we curently need only extract the trust domain -type SpireServerConfig struct { - TrustDomain string `hcl:"trust_domain"` -} - -type SPIREConfig struct { - Server *SpireServerConfig `hcl:"server"` - Plugins ast.Node `hcl:"plugins"` -} - -type TornjakConfig struct { - Server *serverConfig `hcl:"server"` - Plugins *ast.Node `hcl:"plugins"` -} - -/* Server configuration*/ - -type serverConfig struct { - SPIRESocket string `hcl:"spire_socket_path"` - HTTPConfig *HTTPConfig `hcl:"http"` - HTTPSConfig *HTTPSConfig `hcl:"https"` -} - -type HTTPConfig struct { - ListenPort int `hcl:"port"` -} - -type HTTPSConfig struct { - ListenPort int `hcl:"port"` - 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 - - mtls := (clientCAPath != "") - - if _, err := os.Stat(serverCertPath); os.IsNotExist(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 '%s': %w", serverKeyPath, err) - } - - // Create a CA certificate pool and add cert.pem to it - serverCert, err := os.ReadFile(serverCertPath) - if err != nil { - return nil, fmt.Errorf("server ca pool error: %w", err) - } - caCertPool := x509.NewCertPool() - caCertPool.AppendCertsFromPEM(serverCert) - - 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) - } - - // 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 -} - -/* Plugin types */ -type pluginDataStoreSQL struct { - Drivername string `hcl:"drivername"` - Filename string `hcl:"filename"` -} - -type pluginAuthenticatorKeycloak struct { - IssuerURL string `hcl:"issuer"` - Audience string `hcl:"audience"` -} - -type AuthRole struct { - Name string `hcl:",key"` - Desc string `hcl:"desc"` -} - -type APIv1RoleMapping struct { - Name string `hcl:",key"` - Method string `hcl:"-"` - Path string `hcl:"-"` - AllowedRoles []string `hcl:"allowed_roles"` -} - -type pluginAuthorizerRBAC struct { - Name string `hcl:"name"` - RoleList []*AuthRole `hcl:"role,block"` - APIv1RoleMappings []*APIv1RoleMapping `hcl:"APIv1,block"` -} +package api + +import ( + "crypto/tls" + "crypto/x509" + "fmt" + "os" + + "github.com/hashicorp/hcl/hcl/ast" +) + +// TornjakServerInfo provides insight into the configuration of the SPIRE server +// where the Tornjak Agent resides +type TornjakSpireServerInfo struct { + // Plugins is a map from plugin types to respective names of plugins configured + Plugins map[string][]string `json:"plugins"` + // TrustDomain specifies the trust domain of the SPIRE server configured with tornjak + TrustDomain string `json:"trustDomain"` + // Verbose config contains unstructure information on the config on the agent + VerboseConfig string `json:"verboseConfig"` +} + +// pared down version of full Server Config type spire/cmd/spire-server/cli/run +// we curently need only extract the trust domain +type SpireServerConfig struct { + TrustDomain string `hcl:"trust_domain"` +} + +type SPIREConfig struct { + Server *SpireServerConfig `hcl:"server"` + Plugins ast.Node `hcl:"plugins"` +} + +type TornjakConfig struct { + Server *serverConfig `hcl:"server"` + Plugins *ast.Node `hcl:"plugins"` +} + +/* Server configuration*/ + +type serverConfig struct { + SPIRESocket string `hcl:"spire_socket_path"` + HTTPConfig *HTTPConfig `hcl:"http"` + HTTPSConfig *HTTPSConfig `hcl:"https"` +} + +type HTTPConfig struct { + ListenPort int `hcl:"port"` +} + +type HTTPSConfig struct { + ListenPort int `hcl:"port"` + 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 + + mtls := (clientCAPath != "") + + if _, err := os.Stat(serverCertPath); os.IsNotExist(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 '%s': %w", serverKeyPath, err) + } + + // Create a CA certificate pool and add cert.pem to it + serverCert, err := os.ReadFile(serverCertPath) + if err != nil { + return nil, fmt.Errorf("server ca pool error: %w", err) + } + caCertPool := x509.NewCertPool() + caCertPool.AppendCertsFromPEM(serverCert) + + 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) + } + + // 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 +} + +/* Plugin types */ +type pluginDataStoreSQL struct { + Drivername string `hcl:"drivername"` + Filename string `hcl:"filename"` +} + +type pluginControllerManager struct { + Classname string `hcl:"classname"` +} + +type pluginAuthenticatorKeycloak struct { + IssuerURL string `hcl:"issuer"` + Audience string `hcl:"audience"` +} + +type AuthRole struct { + Name string `hcl:",key"` + Desc string `hcl:"desc"` +} + +type APIv1RoleMapping struct { + Name string `hcl:",key"` + Method string `hcl:"-"` + Path string `hcl:"-"` + AllowedRoles []string `hcl:"allowed_roles"` +} + +type pluginAuthorizerRBAC struct { + Name string `hcl:"name"` + RoleList []*AuthRole `hcl:"role,block"` + APIv1RoleMappings []*APIv1RoleMapping `hcl:"APIv1,block"` +} diff --git a/docs/conf/agent/full.conf b/docs/conf/agent/full.conf index 853ed017..c4e54c9b 100644 --- a/docs/conf/agent/full.conf +++ b/docs/conf/agent/full.conf @@ -37,6 +37,16 @@ plugins { } ### END DATASTORE PLUGIN CONFIGURATION + + ### BEGIN SPIRE CRD PLUGIN CONFIGURATION ### + + # Configure CRD management + SPIRECRDManager { + plugin_data { + classname = "spire-mgmt-spire" + } + } + ### END SPIRE CRD PLUGIN CONFIGURATION ### ### BEGIN IAM PLUGIN CONFIGURATION ### # Note: if no UserManagement configuration included, authentication treated as noop diff --git a/docs/config-tornjak-server.md b/docs/config-tornjak-server.md index f5b2bea7..56782e44 100644 --- a/docs/config-tornjak-server.md +++ b/docs/config-tornjak-server.md @@ -72,19 +72,21 @@ Tornjak supports several different plugin types, each representing a different f ### Plugin types -| Type | Description | Required | -|:--------------|:------------|:---------| -| DataStore | Provides persistent storage for Tornjak metadata. | True | -| Authenticator | Verify tokens signed by external OIDC server and extract user information to be passed to the Authorization layer. Any user information or errors from this layer are to be interpreted by an Authorizer layer. | False | -| Authorizer | Based on user information or errors passed from authentication layer and API call details, apply authorization logic. | False | +| Type | Description | Required | +|:----------------|:------------|:---------| +| DataStore | Provides persistent storage for Tornjak metadata. | True | +| SPIRECRDManager | Enables SPIRE CRD Management via Tornjak API. | False | +| Authenticator | Verify tokens signed by external OIDC server and extract user information to be passed to the Authorization layer. Any user information or errors from this layer are to be interpreted by an Authorizer layer. | False | +| Authorizer | Based on user information or errors passed from authentication layer and API call details, apply authorization logic. | False | ### Built-in plugins | Type | Name | Description | | ---- | ---- | ----------- | -| DataStore | SQL | Default SQL storage for Tornjak metadata | -| Authenticator | [keycloak](/docs/plugin_server_authentication_keycloak.md) | Perform OIDC Discovery and extract roles from `realmAccess.roles` field | -| Authorizer | [RBAC](/docs/plugin_server_authorization_rbac.md) | Check api permission based on user role and defined authorization logic | +| DataStore | SQL | Default SQL storage for Tornjak metadata | +| SPIRECRDManager | [""](/docs/plugin_server_spirecrd.md) | CRD Manager | +| Authenticator | [keycloak](/docs/plugin_server_authentication_keycloak.md) | Perform OIDC Discovery and extract roles from `realmAccess.roles` field | +| Authorizer | [RBAC](/docs/plugin_server_authorization_rbac.md) | Check api permission based on user role and defined authorization logic | ### Plugin configuration diff --git a/docs/plugin_server_spirecrd.md b/docs/plugin_server_spirecrd.md new file mode 100644 index 00000000..15c2343f --- /dev/null +++ b/docs/plugin_server_spirecrd.md @@ -0,0 +1,19 @@ +# Server plugin: SPIRECRDManager + +Note the SPIRECRDManager is an optional plugin. This plugin enables the creation of SPIRE CRDs on the cluster Tornjak is deployed on. + +The configuration has the following key-value pairs: + +| Key | Description | Required | +| ---------- | -------------------------------- | ------------------- | +| classname | className label for created CRDs | False | + +A sample configuration file for syntactic reference is below: + +```hcl + SPIREControllerManager { + plugin_data { + classname = "spire-mgmt-spire" + } + } +``` diff --git a/pkg/agent/spirecrd/crdmanager.go b/pkg/agent/spirecrd/crdmanager.go new file mode 100644 index 00000000..77e4b1d2 --- /dev/null +++ b/pkg/agent/spirecrd/crdmanager.go @@ -0,0 +1,17 @@ +package spirecrd + +// CRDManager defines the interface for managing CRDs +type CRDManager interface { + // TODO add List/Create/Update/Delete functions for Federation CRD +} + +type SPIRECRDManager struct { + className string +} + +// NewSPIRECRDManager initializes new SPIRECRDManager +func NewSPIRECRDManager(className string) (*SPIRECRDManager, error) { + return &SPIRECRDManager{ + className: className, + }, nil +}