Skip to content

Commit

Permalink
wip use rpc and default to disabled
Browse files Browse the repository at this point in the history
  • Loading branch information
schmichael committed Oct 3, 2023
1 parent d5c02dc commit 23097a7
Show file tree
Hide file tree
Showing 7 changed files with 103 additions and 59 deletions.
7 changes: 1 addition & 6 deletions command/agent/agent.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 != "" {
Expand Down
2 changes: 1 addition & 1 deletion command/agent/http.go
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down
53 changes: 6 additions & 47 deletions command/agent/keyring_endpoint.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@ package agent
import (
"fmt"
"net/http"
"net/url"
"strings"
"time"

Expand Down Expand Up @@ -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
Expand Down
5 changes: 3 additions & 2 deletions nomad/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
}

Expand Down
21 changes: 21 additions & 0 deletions nomad/keyring_endpoint.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
28 changes: 25 additions & 3 deletions nomad/server.go
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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
Expand Down
46 changes: 46 additions & 0 deletions nomad/structs/keyring.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import (
"crypto/ed25519"
"crypto/x509"
"fmt"
"net/url"
"time"

"github.com/hashicorp/nomad/helper"
Expand All @@ -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.
Expand Down Expand Up @@ -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
}

0 comments on commit 23097a7

Please sign in to comment.