diff --git a/pkg/model/BUILD.bazel b/pkg/model/BUILD.bazel index e60448c8a7..bf1485adca 100644 --- a/pkg/model/BUILD.bazel +++ b/pkg/model/BUILD.bazel @@ -5,6 +5,7 @@ go_library( srcs = [ "bootstrapscript.go", "context.go", + "issuerdiscovery.go", "manifests.go", "master_volumes.go", "names.go", @@ -47,6 +48,7 @@ go_library( "//util/pkg/vfs:go_default_library", "//vendor/github.com/aws/aws-sdk-go/service/ec2:go_default_library", "//vendor/github.com/blang/semver/v4:go_default_library", + "//vendor/gopkg.in/square/go-jose.v2:go_default_library", "//vendor/k8s.io/apimachinery/pkg/runtime:go_default_library", "//vendor/k8s.io/apimachinery/pkg/util/net:go_default_library", "//vendor/k8s.io/apimachinery/pkg/util/yaml:go_default_library", diff --git a/pkg/model/awsmodel/BUILD.bazel b/pkg/model/awsmodel/BUILD.bazel index f68a180f93..4e01bbf00e 100644 --- a/pkg/model/awsmodel/BUILD.bazel +++ b/pkg/model/awsmodel/BUILD.bazel @@ -31,11 +31,9 @@ go_library( "//upup/pkg/fi/cloudup/awstasks:go_default_library", "//upup/pkg/fi/cloudup/awsup:go_default_library", "//upup/pkg/fi/cloudup/spotinsttasks:go_default_library", - "//upup/pkg/fi/fitasks:go_default_library", "//vendor/github.com/aws/aws-sdk-go/aws:go_default_library", "//vendor/github.com/aws/aws-sdk-go/aws/endpoints:go_default_library", "//vendor/github.com/aws/aws-sdk-go/service/ec2:go_default_library", - "//vendor/gopkg.in/square/go-jose.v2:go_default_library", "//vendor/k8s.io/api/core/v1:go_default_library", "//vendor/k8s.io/apimachinery/pkg/util/sets:go_default_library", "//vendor/k8s.io/klog/v2:go_default_library", diff --git a/pkg/model/awsmodel/oidc_provider.go b/pkg/model/awsmodel/oidc_provider.go index ea685239a9..85c74f6e84 100644 --- a/pkg/model/awsmodel/oidc_provider.go +++ b/pkg/model/awsmodel/oidc_provider.go @@ -17,21 +17,10 @@ limitations under the License. package awsmodel import ( - "bytes" - "crypto" - "crypto/x509" - "encoding/base64" - "encoding/json" - "encoding/pem" - "fmt" - "io" - - "gopkg.in/square/go-jose.v2" "k8s.io/kops/pkg/featureflag" "k8s.io/kops/pkg/model/iam" "k8s.io/kops/upup/pkg/fi" "k8s.io/kops/upup/pkg/fi/cloudup/awstasks" - "k8s.io/kops/upup/pkg/fi/fitasks" ) // OIDCProviderBuilder configures IAM OIDC Provider @@ -41,16 +30,6 @@ type OIDCProviderBuilder struct { Lifecycle *fi.Lifecycle } -type oidcDiscovery struct { - Issuer string `json:"issuer"` - JWKSURI string `json:"jwks_uri"` - AuthorizationEndpoint string `json:"authorization_endpoint"` - ResponseTypes []string `json:"response_types_supported"` - SubjectTypes []string `json:"subject_types_supported"` - SigningAlgs []string `json:"id_token_signing_alg_values_supported"` - ClaimsSupported []string `json:"claims_supported"` -} - var _ fi.ModelBuilder = &OIDCProviderBuilder{} const ( @@ -68,11 +47,6 @@ func (b *OIDCProviderBuilder) Build(c *fi.ModelBuilderContext) error { return err } - signingKeyTaskObject, found := c.Tasks["Keypair/service-account"] - if !found { - return fmt.Errorf("keypair/service-account task not found") - } - fingerprints := getFingerprints() thumbprints := []*string{} @@ -81,36 +55,6 @@ func (b *OIDCProviderBuilder) Build(c *fi.ModelBuilderContext) error { thumbprints = append(thumbprints, fi.String(fingerprint)) } - skTask := signingKeyTaskObject.(*fitasks.Keypair) - - keys := &OIDCKeys{ - SigningKey: skTask, - } - - discovery, err := buildDiscoveryJSON(serviceAccountIssuer) - if err != nil { - return err - } - keysFile := &fitasks.ManagedFile{ - Contents: keys, - Lifecycle: b.Lifecycle, - Location: fi.String("oidc/keys.json"), - Name: fi.String("keys.json"), - Base: fi.String(b.Cluster.Spec.PublicDataStore), - Public: fi.Bool(true), - } - c.AddTask(keysFile) - - discoveryFile := &fitasks.ManagedFile{ - Contents: fi.NewBytesResource(discovery), - Lifecycle: b.Lifecycle, - Location: fi.String("oidc/.well-known/openid-configuration"), - Name: fi.String("discovery.json"), - Base: fi.String(b.Cluster.Spec.PublicDataStore), - Public: fi.Bool(true), - } - c.AddTask(discoveryFile) - c.AddTask(&awstasks.IAMOIDCProvider{ Name: fi.String(b.ClusterName()), Lifecycle: b.Lifecycle, @@ -123,76 +67,6 @@ func (b *OIDCProviderBuilder) Build(c *fi.ModelBuilderContext) error { return nil } -func buildDiscoveryJSON(issuerURL string) ([]byte, error) { - d := oidcDiscovery{ - Issuer: fmt.Sprintf("%v/", issuerURL), - JWKSURI: fmt.Sprintf("%v/keys.json", issuerURL), - AuthorizationEndpoint: "urn:kubernetes:programmatic_authorization", - ResponseTypes: []string{"id_token"}, - SubjectTypes: []string{"public"}, - SigningAlgs: []string{"RS256"}, - ClaimsSupported: []string{"sub", "iss"}, - } - return json.MarshalIndent(d, "", "") -} - -type KeyResponse struct { - Keys []jose.JSONWebKey `json:"keys"` -} - -type OIDCKeys struct { - SigningKey *fitasks.Keypair -} - -// GetDependencies adds CA to the list of dependencies -func (o *OIDCKeys) GetDependencies(tasks map[string]fi.Task) []fi.Task { - return []fi.Task{ - o.SigningKey, - } -} -func (o *OIDCKeys) Open() (io.Reader, error) { - - certBytes, err := fi.ResourceAsBytes(o.SigningKey.Certificate()) - if err != nil { - return nil, fmt.Errorf("failed to get cert: %w", err) - } - block, _ := pem.Decode(certBytes) - cert, err := x509.ParseCertificate(block.Bytes) - if err != nil { - return nil, fmt.Errorf("failed to parse cert: %w", err) - } - - publicKey := cert.PublicKey - - publicKeyDERBytes, err := x509.MarshalPKIXPublicKey(publicKey) - if err != nil { - return nil, fmt.Errorf("failed to serialize public key to DER format: %v", err) - } - - hasher := crypto.SHA256.New() - hasher.Write(publicKeyDERBytes) - publicKeyDERHash := hasher.Sum(nil) - - keyID := base64.RawURLEncoding.EncodeToString(publicKeyDERHash) - - keys := []jose.JSONWebKey{ - { - Key: publicKey, - KeyID: keyID, - Algorithm: string(jose.RS256), - Use: "sig", - }, - } - - keyResponse := KeyResponse{Keys: keys} - jsonBytes, err := json.MarshalIndent(keyResponse, "", "") - if err != nil { - return nil, fmt.Errorf("failed to marshal json: %w", err) - } - - return bytes.NewReader(jsonBytes), nil -} - func getFingerprints() []string { //These strings are the sha1 of the two possible S3 root CAs. diff --git a/pkg/model/issuerdiscovery.go b/pkg/model/issuerdiscovery.go new file mode 100644 index 0000000000..dd76ad4b3d --- /dev/null +++ b/pkg/model/issuerdiscovery.go @@ -0,0 +1,171 @@ +/* +Copyright 2019 The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package model + +import ( + "bytes" + "crypto" + "crypto/x509" + "encoding/base64" + "encoding/json" + "encoding/pem" + "fmt" + "io" + + "gopkg.in/square/go-jose.v2" + "k8s.io/kops/pkg/apis/kops" + "k8s.io/kops/pkg/featureflag" + "k8s.io/kops/pkg/model/iam" + "k8s.io/kops/upup/pkg/fi" + "k8s.io/kops/upup/pkg/fi/fitasks" +) + +// IssuerDiscoveryModelBuilder publish OIDC issuer discovery metadata +type IssuerDiscoveryModelBuilder struct { + *KopsModelContext + + Lifecycle *fi.Lifecycle + Cluster *kops.Cluster +} + +type oidcDiscovery struct { + Issuer string `json:"issuer"` + JWKSURI string `json:"jwks_uri"` + AuthorizationEndpoint string `json:"authorization_endpoint"` + ResponseTypes []string `json:"response_types_supported"` + SubjectTypes []string `json:"subject_types_supported"` + SigningAlgs []string `json:"id_token_signing_alg_values_supported"` + ClaimsSupported []string `json:"claims_supported"` +} + +func (b *IssuerDiscoveryModelBuilder) Build(c *fi.ModelBuilderContext) error { + if !featureflag.PublicJWKS.Enabled() { + return nil + } + + signingKeyTaskObject, found := c.Tasks["Keypair/service-account"] + if !found { + return fmt.Errorf("keypair/service-account task not found") + } + + skTask := signingKeyTaskObject.(*fitasks.Keypair) + + keys := &OIDCKeys{ + SigningKey: skTask, + } + + serviceAccountIssuer, err := iam.ServiceAccountIssuer(&b.Cluster.Spec) + if err != nil { + return err + } + + discovery, err := buildDiscoveryJSON(serviceAccountIssuer) + if err != nil { + return err + } + keysFile := &fitasks.ManagedFile{ + Contents: keys, + Lifecycle: b.Lifecycle, + Location: fi.String("/openid/v1/jwks"), + Name: fi.String("keys.json"), + Base: fi.String(b.Cluster.Spec.PublicDataStore), + Public: fi.Bool(true), + } + c.AddTask(keysFile) + + discoveryFile := &fitasks.ManagedFile{ + Contents: fi.NewBytesResource(discovery), + Lifecycle: b.Lifecycle, + Location: fi.String("oidc/.well-known/openid-configuration"), + Name: fi.String("discovery.json"), + Base: fi.String(b.Cluster.Spec.PublicDataStore), + Public: fi.Bool(true), + } + c.AddTask(discoveryFile) + + return nil +} + +func buildDiscoveryJSON(issuerURL string) ([]byte, error) { + d := oidcDiscovery{ + Issuer: fmt.Sprintf("%v/", issuerURL), + JWKSURI: fmt.Sprintf("%v/openid/v1/jwks", issuerURL), + AuthorizationEndpoint: "urn:kubernetes:programmatic_authorization", + ResponseTypes: []string{"id_token"}, + SubjectTypes: []string{"public"}, + SigningAlgs: []string{"RS256"}, + ClaimsSupported: []string{"sub", "iss"}, + } + return json.MarshalIndent(d, "", "") +} + +type KeyResponse struct { + Keys []jose.JSONWebKey `json:"keys"` +} + +type OIDCKeys struct { + SigningKey *fitasks.Keypair +} + +// GetDependencies adds CA to the list of dependencies +func (o *OIDCKeys) GetDependencies(tasks map[string]fi.Task) []fi.Task { + return []fi.Task{ + o.SigningKey, + } +} +func (o *OIDCKeys) Open() (io.Reader, error) { + + certBytes, err := fi.ResourceAsBytes(o.SigningKey.Certificate()) + if err != nil { + return nil, fmt.Errorf("failed to get cert: %w", err) + } + block, _ := pem.Decode(certBytes) + cert, err := x509.ParseCertificate(block.Bytes) + if err != nil { + return nil, fmt.Errorf("failed to parse cert: %w", err) + } + + publicKey := cert.PublicKey + + publicKeyDERBytes, err := x509.MarshalPKIXPublicKey(publicKey) + if err != nil { + return nil, fmt.Errorf("failed to serialize public key to DER format: %v", err) + } + + hasher := crypto.SHA256.New() + hasher.Write(publicKeyDERBytes) + publicKeyDERHash := hasher.Sum(nil) + + keyID := base64.RawURLEncoding.EncodeToString(publicKeyDERHash) + + keys := []jose.JSONWebKey{ + { + Key: publicKey, + KeyID: keyID, + Algorithm: string(jose.RS256), + Use: "sig", + }, + } + + keyResponse := KeyResponse{Keys: keys} + jsonBytes, err := json.MarshalIndent(keyResponse, "", "") + if err != nil { + return nil, fmt.Errorf("failed to marshal json: %w", err) + } + + return bytes.NewReader(jsonBytes), nil +} diff --git a/upup/pkg/fi/cloudup/apply_cluster.go b/upup/pkg/fi/cloudup/apply_cluster.go index 8e3d53f714..fed54b7cdf 100644 --- a/upup/pkg/fi/cloudup/apply_cluster.go +++ b/upup/pkg/fi/cloudup/apply_cluster.go @@ -527,6 +527,11 @@ func (c *ApplyClusterCmd) Run(ctx context.Context) error { KopsModelContext: modelContext, Lifecycle: &clusterLifecycle, }, + &model.IssuerDiscoveryModelBuilder{ + KopsModelContext: modelContext, + Lifecycle: &clusterLifecycle, + Cluster: cluster, + }, &kubeapiserver.KubeApiserverBuilder{ AssetBuilder: assetBuilder, KopsModelContext: modelContext,