Skip to content

Commit

Permalink
Expose JWKS via a feature-flag
Browse files Browse the repository at this point in the history
When the PublicJWKS feature-flag is set, we expose the apiserver JWKS
document publicly (including enabling anonymous access).  This is a
stepping stone to a more hardened configuration where we copy the JWKS
document to S3/GCS/etc.
  • Loading branch information
justinsb committed Aug 26, 2020
1 parent 5179c27 commit be0da5a
Show file tree
Hide file tree
Showing 39 changed files with 2,356 additions and 36 deletions.
26 changes: 22 additions & 4 deletions cloudmock/aws/mockiam/oidcprovider.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ limitations under the License.
package mockiam

import (
"context"
"fmt"

"github.com/aws/aws-sdk-go/aws"
Expand Down Expand Up @@ -48,11 +49,28 @@ func (m *MockIAM) ListOpenIDConnectProvidersRequest(*iam.ListOpenIDConnectProvid
panic("Not implemented")
}

func (m *MockIAM) GetOpenIDConnectProviderWithContext(aws.Context, *iam.GetOpenIDConnectProviderInput, ...request.Option) (*iam.GetOpenIDConnectProviderOutput, error) {
panic("Not implemented")
func (m *MockIAM) GetOpenIDConnectProviderWithContext(ctx aws.Context, request *iam.GetOpenIDConnectProviderInput, options ...request.Option) (*iam.GetOpenIDConnectProviderOutput, error) {
m.mutex.Lock()
defer m.mutex.Unlock()

arn := aws.StringValue(request.OpenIDConnectProviderArn)

provider := m.OIDCProviders[arn]
if provider == nil {
return nil, fmt.Errorf("OpenIDConnectProvider with arn=%q not found", arn)
}

response := &iam.GetOpenIDConnectProviderOutput{
ClientIDList: provider.ClientIDList,
CreateDate: provider.CreateDate,
ThumbprintList: provider.ThumbprintList,
Url: provider.Url,
}
return response, nil
}
func (m *MockIAM) GetOpenIDConnectProvider(*iam.GetOpenIDConnectProviderInput) (*iam.GetOpenIDConnectProviderOutput, error) {
panic("Not implemented")

func (m *MockIAM) GetOpenIDConnectProvider(request *iam.GetOpenIDConnectProviderInput) (*iam.GetOpenIDConnectProviderOutput, error) {
return m.GetOpenIDConnectProviderWithContext(context.Background(), request)
}

func (m *MockIAM) GetOpenIDConnectProviderRequest(*iam.GetOpenIDConnectProviderInput) (*request.Request, *iam.GetOpenIDConnectProviderOutput) {
Expand Down
43 changes: 38 additions & 5 deletions cmd/kops/integration_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -61,8 +61,10 @@ type integrationTest struct {
launchConfiguration bool
lifecycleOverrides []string
sshKey bool
jsonOutput bool
bastionUserData bool
// caKey is true if we should use a provided ca.crt & ca.key as our CA
caKey bool
jsonOutput bool
bastionUserData bool
}

func newIntegrationTest(clusterName, srcDir string) *integrationTest {
Expand Down Expand Up @@ -91,6 +93,13 @@ func (i *integrationTest) withoutSSHKey() *integrationTest {
return i
}

// withCAKey indicates that we should use a fixed ca.crt & ca.key from the source directory as our CA.
// This is needed when the CA is exposed, for example when using AWS WebIdentity federation.
func (i *integrationTest) withCAKey() *integrationTest {
i.caKey = true
return i
}

func (i *integrationTest) withoutPolicies() *integrationTest {
i.expectPolicies = false
return i
Expand Down Expand Up @@ -314,15 +323,27 @@ func TestContainerdCloudformation(t *testing.T) {
// TestLaunchConfigurationASG tests ASGs using launch configurations instead of launch templates
func TestLaunchConfigurationASG(t *testing.T) {
featureflag.ParseFlags("-EnableLaunchTemplates")
unsetFeaureFlag := func() {
unsetFeatureFlags := func() {
featureflag.ParseFlags("+EnableLaunchTemplates")
}
defer unsetFeaureFlag()
defer unsetFeatureFlags()

newIntegrationTest("launchtemplates.example.com", "launch_templates").withZones(3).withLaunchConfiguration().runTestTerraformAWS(t)
newIntegrationTest("launchtemplates.example.com", "launch_templates").withZones(3).withLaunchConfiguration().runTestCloudformation(t)
}

// TestJWKS runs a simple configuration, but with PublicJWKS enabled
func TestJWKS(t *testing.T) {
featureflag.ParseFlags("+PublicJWKS")
unsetFeatureFlags := func() {
featureflag.ParseFlags("-PublicJWKS")
}
defer unsetFeatureFlags()

// We have to use a fixed CA because the fingerprint is inserted into the AWS WebIdentity configuration.
newIntegrationTest("minimal.example.com", "jwks").withCAKey().runTestTerraformAWS(t)
}

func (i *integrationTest) runTest(t *testing.T, h *testutils.IntegrationTestHarness, expectedDataFilenames []string, tfFileName string, expectedTfFileName string, phase *cloudup.Phase) {
ctx := context.Background()

Expand Down Expand Up @@ -364,7 +385,19 @@ func (i *integrationTest) runTest(t *testing.T, h *testutils.IntegrationTestHarn

err := RunCreateSecretPublicKey(ctx, factory, &stdout, options)
if err != nil {
t.Fatalf("error running %q create: %v", inputYAML, err)
t.Fatalf("error running %q create public key: %v", inputYAML, err)
}
}

if i.caKey {
options := &CreateSecretCaCertOptions{}
options.ClusterName = i.clusterName
options.CaPrivateKeyPath = path.Join(i.srcDir, "ca.key")
options.CaCertPath = path.Join(i.srcDir, "ca.crt")

err := RunCreateSecretCaCert(ctx, factory, &stdout, options)
if err != nil {
t.Fatalf("error running %q create CA keypair: %v", inputYAML, err)
}
}

Expand Down
2 changes: 2 additions & 0 deletions pkg/featureflag/featureflag.go
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,8 @@ var (
Terraform012 = New("Terraform-0.12", Bool(true))
// LegacyIAM will permit use of legacy IAM permissions.
LegacyIAM = New("LegacyIAM", Bool(false))
// PublicJWKS enables public jwks access. This is generally not as secure as republishing.
PublicJWKS = New("PublicJWKS", Bool(false))
)

// FeatureFlag defines a feature flag
Expand Down
3 changes: 3 additions & 0 deletions pkg/model/awsmodel/BUILD.bazel
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ go_library(
"api_loadbalancer.go",
"autoscalinggroup.go",
"context.go",
"oidc_provider.go",
],
importpath = "k8s.io/kops/pkg/model/awsmodel",
visibility = ["//visibility:public"],
Expand All @@ -15,9 +16,11 @@ go_library(
"//pkg/featureflag:go_default_library",
"//pkg/model:go_default_library",
"//pkg/model/defaults:go_default_library",
"//pkg/model/iam:go_default_library",
"//pkg/model/spotinstmodel:go_default_library",
"//upup/pkg/fi:go_default_library",
"//upup/pkg/fi/cloudup/awstasks:go_default_library",
"//upup/pkg/fi/fitasks:go_default_library",
"//vendor/github.com/aws/aws-sdk-go/service/ec2:go_default_library",
"//vendor/k8s.io/apimachinery/pkg/util/sets:go_default_library",
"//vendor/k8s.io/klog/v2:go_default_library",
Expand Down
79 changes: 79 additions & 0 deletions pkg/model/awsmodel/oidc_provider.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
/*
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 awsmodel

import (
"fmt"

"k8s.io/kops/pkg/featureflag"
"k8s.io/kops/pkg/model"
"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
type OIDCProviderBuilder struct {
*model.KopsModelContext

KeyStore fi.CAStore
Lifecycle *fi.Lifecycle
}

var _ fi.ModelBuilder = &OIDCProviderBuilder{}

const (
defaultAudience = "amazonaws.com"
)

func (b *OIDCProviderBuilder) Build(c *fi.ModelBuilderContext) error {
var thumbprints []fi.Resource
var issuerURL string

if featureflag.PublicJWKS.Enabled() {
serviceAccountIssuer, err := iam.ServiceAccountIssuer(b.ClusterName(), &b.Cluster.Spec)
if err != nil {
return err
}
issuerURL = serviceAccountIssuer

caTaskObject, found := c.Tasks["Keypair/ca"]
if !found {
return fmt.Errorf("keypair/ca task not found")
}

caTask := caTaskObject.(*fitasks.Keypair)
fingerprint := caTask.CertificateSHA1Fingerprint()

thumbprints = []fi.Resource{fingerprint}
}

if issuerURL == "" {
return nil
}

c.AddTask(&awstasks.IAMOIDCProvider{
Name: fi.String(b.ClusterName()),
Lifecycle: b.Lifecycle,
URL: fi.String(issuerURL),
ClientIDs: []*string{fi.String(defaultAudience)},
Thumbprints: thumbprints,
})

return nil
}
3 changes: 3 additions & 0 deletions pkg/model/components/BUILD.bazel
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ go_library(
"containerd.go",
"context.go",
"defaults.go",
"discovery.go",
"docker.go",
"etcd.go",
"kubecontrollermanager.go",
Expand All @@ -24,7 +25,9 @@ go_library(
"//pkg/apis/kops:go_default_library",
"//pkg/apis/kops/util:go_default_library",
"//pkg/assets:go_default_library",
"//pkg/featureflag:go_default_library",
"//pkg/k8sversion:go_default_library",
"//pkg/model/iam:go_default_library",
"//pkg/wellknownports:go_default_library",
"//upup/pkg/fi:go_default_library",
"//upup/pkg/fi/cloudup/gce:go_default_library",
Expand Down
7 changes: 7 additions & 0 deletions pkg/model/components/apiserver.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import (

v1 "k8s.io/api/core/v1"
"k8s.io/kops/pkg/apis/kops"
"k8s.io/kops/pkg/featureflag"
"k8s.io/kops/upup/pkg/fi"
"k8s.io/kops/upup/pkg/fi/loader"

Expand Down Expand Up @@ -208,6 +209,12 @@ func (b *KubeAPIServerOptionsBuilder) BuildOptions(o interface{}) error {
// We make sure to disable AnonymousAuth
c.AnonymousAuth = fi.Bool(false)

// For the development of JWKS functionality, re-enable anonymous auth for PublicJWKS
// (this likely isn't a production-suitable configuration, currently)
if featureflag.PublicJWKS.Enabled() {
c.AnonymousAuth = fi.Bool(true)
}

if b.IsKubernetesGTE("1.17") {
// We query via the kube-apiserver-healthcheck proxy, which listens on port 3990
c.InsecurePort = 0
Expand Down
83 changes: 83 additions & 0 deletions pkg/model/components/discovery.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
/*
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 components

import (
"strings"

"k8s.io/kops/pkg/apis/kops"
"k8s.io/kops/pkg/featureflag"
"k8s.io/kops/pkg/model/iam"
"k8s.io/kops/upup/pkg/fi/loader"
)

// DiscoveryOptionsBuilder adds options for identity discovery to the model (mostly kube-apiserver)
type DiscoveryOptionsBuilder struct {
*OptionsContext
}

var _ loader.OptionsBuilder = &DiscoveryOptionsBuilder{}

func (b *DiscoveryOptionsBuilder) BuildOptions(o interface{}) error {
clusterSpec := o.(*kops.ClusterSpec)

useJWKS := featureflag.PublicJWKS.Enabled()
if !useJWKS {
return nil
}

if clusterSpec.KubeAPIServer == nil {
clusterSpec.KubeAPIServer = &kops.KubeAPIServerConfig{}
}

kubeAPIServer := clusterSpec.KubeAPIServer

if kubeAPIServer.FeatureGates == nil {
kubeAPIServer.FeatureGates = make(map[string]string)
}
kubeAPIServer.FeatureGates["ServiceAccountIssuerDiscovery"] = "true"

if len(kubeAPIServer.APIAudiences) == 0 {
kubeAPIServer.APIAudiences = []string{"kubernetes.svc.default"}
}

if kubeAPIServer.ServiceAccountIssuer == nil {
serviceAccountIssuer, err := iam.ServiceAccountIssuer(b.ClusterName, clusterSpec)
if err != nil {
return err
}
kubeAPIServer.ServiceAccountIssuer = &serviceAccountIssuer
}

if kubeAPIServer.ServiceAccountJWKSURI == nil {
jwksURL := *kubeAPIServer.ServiceAccountIssuer
jwksURL = strings.TrimSuffix(jwksURL, "/") + "/openid/v1/jwks"

kubeAPIServer.ServiceAccountJWKSURI = &jwksURL
}

if kubeAPIServer.ServiceAccountSigningKeyFile == nil {
s := "/srv/kubernetes/server.key"
kubeAPIServer.ServiceAccountSigningKeyFile = &s
}

if len(kubeAPIServer.ServiceAccountKeyFile) == 0 {
kubeAPIServer.ServiceAccountKeyFile = []string{"/srv/kubernetes/server.key"}
}

return nil
}
2 changes: 2 additions & 0 deletions pkg/model/iam/BUILD.bazel
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,15 @@ go_library(
name = "go_default_library",
srcs = [
"iam_builder.go",
"pod_roles.go",
"types.go",
],
importpath = "k8s.io/kops/pkg/model/iam",
visibility = ["//visibility:public"],
deps = [
"//pkg/apis/kops:go_default_library",
"//pkg/apis/kops/model:go_default_library",
"//pkg/featureflag:go_default_library",
"//pkg/util/stringorslice:go_default_library",
"//upup/pkg/fi:go_default_library",
"//upup/pkg/fi/cloudup/awstasks:go_default_library",
Expand Down
Loading

0 comments on commit be0da5a

Please sign in to comment.