From 23097a7083131f0defc4b54083974deeb7f07cf2 Mon Sep 17 00:00:00 2001 From: Michael Schurter Date: Tue, 3 Oct 2023 13:47:56 -0700 Subject: [PATCH] wip use rpc and default to disabled --- command/agent/agent.go | 7 +--- command/agent/http.go | 2 +- command/agent/keyring_endpoint.go | 53 ++++--------------------------- nomad/config.go | 5 +-- nomad/keyring_endpoint.go | 21 ++++++++++++ nomad/server.go | 28 ++++++++++++++-- nomad/structs/keyring.go | 46 +++++++++++++++++++++++++++ 7 files changed, 103 insertions(+), 59 deletions(-) diff --git a/command/agent/agent.go b/command/agent/agent.go index a21572a5bd6..f34f184df7c 100644 --- a/command/agent/agent.go +++ b/command/agent/agent.go @@ -389,12 +389,7 @@ func convertServerConfig(agentConfig *Config) (*nomad.Config, error) { conf.ServerRPCAdvertise = serverAddr // OIDC Issuer address - //FIXME(schmichael) seems like a bad way to configure http for servers. will upgrades break? - if agentConfig.OIDCIssuer == "" { - conf.OIDCIssuer = agentConfig.HTTPAddr() - } else { - conf.OIDCIssuer = agentConfig.OIDCIssuer - } + conf.OIDCIssuer = agentConfig.OIDCIssuer // Set up gc threshold and heartbeat grace period if gcThreshold := agentConfig.Server.NodeGCThreshold; gcThreshold != "" { diff --git a/command/agent/http.go b/command/agent/http.go index 672dcb2ea5b..4f7e20f07b3 100644 --- a/command/agent/http.go +++ b/command/agent/http.go @@ -503,7 +503,7 @@ func (s *HTTPServer) registerHandlers(enableDebug bool) { s.mux.Handle("/v1/var/", wrapCORSWithAllowedMethods(s.wrap(s.VariableSpecificRequest), "HEAD", "GET", "PUT", "DELETE")) // OIDC Handlers - s.mux.HandleFunc("/.well-known/jwks.json", s.wrap(s.JWKSRequest)) + s.mux.HandleFunc(structs.JWKSPath, s.wrap(s.JWKSRequest)) s.mux.HandleFunc("/.well-known/openid-configuration", s.wrap(s.OIDCDiscoveryRequest)) agentConfig := s.agent.GetConfig() diff --git a/command/agent/keyring_endpoint.go b/command/agent/keyring_endpoint.go index b64b9957ccb..4daecc0a5fa 100644 --- a/command/agent/keyring_endpoint.go +++ b/command/agent/keyring_endpoint.go @@ -6,7 +6,6 @@ package agent import ( "fmt" "net/http" - "net/url" "strings" "time" @@ -91,56 +90,16 @@ func (s *HTTPServer) OIDCDiscoveryRequest(resp http.ResponseWriter, req *http.Re if s.parse(resp, req, &args.Region, &args.QueryOptions) { return nil, nil } - - conf := s.agent.GetConfig() - - //FIXME(schmichael) should we bother implementing an RPC just to get region - //forwarding? I think *not* since consumers of this endpoint are code that is - //intended to be talking to a specific region directly. - if args.Region != conf.Region { - return nil, CodedError(400, "Region mismatch") - } - - issuer := conf.HTTPAddr() - if conf.OIDCIssuer != "" { - issuer = conf.OIDCIssuer - } - - //FIXME(schmichael) make a real struct - // stolen from vault/identity_store_oidc_provider.go - type providerDiscovery struct { - Issuer string `json:"issuer,omitempty"` - Keys string `json:"jwks_uri"` - RequestParameter bool `json:"request_parameter_supported"` - RequestURIParameter bool `json:"request_uri_parameter_supported"` - IDTokenAlgs []string `json:"id_token_signing_alg_values_supported,omitempty"` - ResponseTypes []string `json:"response_types_supported,omitempty"` - Subjects []string `json:"subject_types_supported,omitempty"` - //AuthorizationEndpoint string `json:"authorization_endpoint,omitempty"` - //Scopes []string `json:"scopes_supported,omitempty"` - //UserinfoEndpoint string `json:"userinfo_endpoint,omitempty"` - //TokenEndpoint string `json:"token_endpoint,omitempty"` - //Claims []string `json:"claims_supported,omitempty"` - //GrantTypes []string `json:"grant_types_supported,omitempty"` - //AuthMethods []string `json:"token_endpoint_auth_methods_supported,omitempty"` - } - - jwksPath, err := url.JoinPath(issuer, "/.well-known/jwks.json") - if err != nil { - return nil, fmt.Errorf("error determining jwks path: %w", err) + var rpcReply structs.KeyringGetConfigResponse + if err := s.agent.RPC("Keyring.GetConfig", &args, &rpcReply); err != nil { + return nil, err } - disc := providerDiscovery{ - Issuer: issuer, - Keys: jwksPath, - RequestParameter: false, - RequestURIParameter: false, - IDTokenAlgs: []string{structs.PubKeyAlgRS256, structs.PubKeyAlgEdDSA}, - ResponseTypes: []string{"code"}, - Subjects: []string{"public"}, + if rpcReply.OIDCDiscovery == nil { + return nil, CodedError(http.StatusNotFound, "OIDC Discovery endpoint disabled") } - return disc, nil + return rpcReply.OIDCDiscovery, nil } // KeyringRequest is used route operator/raft API requests to the implementing diff --git a/nomad/config.go b/nomad/config.go index 6059ef95b34..3de0852e7a7 100644 --- a/nomad/config.go +++ b/nomad/config.go @@ -432,8 +432,9 @@ type Config struct { Reporting *config.ReportingConfig - // OIDCIssuer is the URL for the OIDC Issuer field in Workload Identity JWTs - //FIXME(schmichael) is this the best way to pass it in? + // OIDCIssuer is the URL for the OIDC Issuer field in Workload Identity JWTs. + // If this is not configured the /.well-known/openid-configuration endpoint + // will not be available. OIDCIssuer string } diff --git a/nomad/keyring_endpoint.go b/nomad/keyring_endpoint.go index 4bae3e38e21..5e0a12b4fba 100644 --- a/nomad/keyring_endpoint.go +++ b/nomad/keyring_endpoint.go @@ -423,3 +423,24 @@ func (k *Keyring) ListPublic(args *structs.GenericRequest, reply *structs.Keyrin } return k.srv.blockingRPC(&opts) } + +// GetConfig for workload identities. This RPC is used to back an OIDC +// Discovery endpoint. +// +// Unauthenticated because OIDC Discovery endpoints must be publically +// available. +func (k *Keyring) GetConfig(args *structs.GenericRequest, reply *structs.KeyringGetConfigResponse) error { + + // JWKS is a public endpoint: intentionally ignore auth errors and only + // authenticate to measure rate metrics. + k.srv.Authenticate(k.ctx, args) + if done, err := k.srv.forward("Keyring.GetConfig", args, args, reply); done { + return err + } + k.srv.MeasureRPCRate("keyring", structs.RateMetricList, args) + + defer metrics.MeasureSince([]string{"nomad", "keyring", "get_config"}, time.Now()) + + reply.OIDCDiscovery = k.srv.oidcDisco + return nil +} diff --git a/nomad/server.go b/nomad/server.go index c8e02ad06be..43c5448ea39 100644 --- a/nomad/server.go +++ b/nomad/server.go @@ -295,6 +295,15 @@ type Server struct { // dependencies. reportingManager *reporting.Manager + // oidcDisco is the OIDC Discovery configuration to be returned by the + // Keyring.GetConfig RPC and /.well-known/openid-configuration HTTP API. + // + // The issuer and jwks url are user configurable and therefore the struct is + // initialized when NewServer is setup. + // + // MAY BE nil! Issuer must be explicitly configured by the end user. + oidcDisco *structs.OIDCDiscoveryConfig + // EnterpriseState is used to fill in state for Pro/Ent builds EnterpriseState @@ -415,9 +424,22 @@ func NewServer(config *Config, consulCatalog consul.CatalogAPI, consulConfigEntr } s.encrypter = encrypter - // Set up the OIDC provider cache. This is needed by the setupRPC, but must - // be done separately so that the server can stop all background processes - // when it shuts down itself. + // Set up the OIDC discovery configuration required by third parties, such as + // AWS's IAM OIDC Provider, to authenticate workload identity JWTs. + if iss := config.OIDCIssuer; iss != "" { + oidcDisco, err := structs.NewOIDCDiscoveryConfig(iss) + if err != nil { + return nil, err + } + s.oidcDisco = oidcDisco + s.logger.Info("issuer set; OIDC Discovery endpoint enabled", "issuer", iss) + } else { + s.logger.Info("issuer not set; OIDC Discovery endpoint disabled") + } + + // Set up the SSO OIDC provider cache. This is needed by the setupRPC, but + // must be done separately so that the server can stop all background + // processes when it shuts down itself. s.oidcProviderCache = oidc.NewProviderCache() // Initialize the RPC layer diff --git a/nomad/structs/keyring.go b/nomad/structs/keyring.go index 4ad2e364373..d5fc6a831e5 100644 --- a/nomad/structs/keyring.go +++ b/nomad/structs/keyring.go @@ -7,6 +7,7 @@ import ( "crypto/ed25519" "crypto/x509" "fmt" + "net/url" "time" "github.com/hashicorp/nomad/helper" @@ -26,6 +27,9 @@ const ( // PubKeyUseSig is the JWK (JSON Web Key) "use" parameter value for // signatures. PubKeyUseSig = "sig" + + // JWKSPath is the path component of the URL to Nomad's JWKS endpoint. + JWKSPath = "/.well-known/jwks.json" ) // RootKey is used to encrypt and decrypt variables. It is never stored in raft. @@ -327,3 +331,45 @@ func (pubKey *KeyringPublicKey) GetPublicKey() (any, error) { return nil, fmt.Errorf("unknown algorithm: %q", alg) } } + +// KeyringGetConfigResponse is the response for Keyring.GetConfig RPCs. +type KeyringGetConfigResponse struct { + OIDCDiscovery *OIDCDiscoveryConfig +} + +// OIDCDiscoveryConfig represents the response to OIDC Discovery requests +// usually at: /.well-known/openid-configuration +// +// Only the fields Nomad uses are implemented since many fields in the +// specification are not relevant to Nomad's use case: +// https://openid.net/specs/openid-connect-discovery-1_0.html +type OIDCDiscoveryConfig struct { + Issuer string `json:"issuer"` + JWKS string `json:"jwks_uri"` + IDTokenAlgs []string `json:"id_token_signing_alg_values_supported"` + ResponseTypes []string `json:"response_types_supported"` + Subjects []string `json:"subject_types_supported"` +} + +// NewOIDCDiscoveryConfig returns a populated OIDCDiscoveryConfig or an error. +func NewOIDCDiscoveryConfig(issuer string) (*OIDCDiscoveryConfig, error) { + jwksURL, err := url.JoinPath(issuer, JWKSPath) + if err != nil { + return nil, fmt.Errorf("error determining jwks path: %w", err) + } + + disc := &OIDCDiscoveryConfig{ + Issuer: issuer, + JWKS: jwksURL, + + // RS256 is required by the OIDC spec and some third parties such as AWS's + // IAM OIDC Provider. Prior to v1.7 Nomad default to EdDSA so advertise + // support for backward compatibility. + IDTokenAlgs: []string{PubKeyAlgRS256, PubKeyAlgEdDSA}, + + ResponseTypes: []string{"code"}, + Subjects: []string{"public"}, + } + + return disc, nil +}