Skip to content
This repository has been archived by the owner on May 10, 2019. It is now read-only.

Add cluster CA certificate into kubeconfig if one exists #36

Merged
merged 3 commits into from
May 16, 2018
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 6 additions & 2 deletions glide.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions glide.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,8 @@ import:
subpackages:
- tools/clientcmd
- tools/clientcmd/api
- package: github.com/spf13/afero
version: ^1.1.0
testImport:
- package: github.com/go-test/deep
version: v1.0.0
28 changes: 27 additions & 1 deletion kuberos.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,16 +4,20 @@ import (
"crypto/sha256"
"encoding/json"
"fmt"
"io/ioutil"
"net/http"
"net/url"
"path/filepath"

"github.com/negz/kuberos/extractor"

oidc "github.com/coreos/go-oidc"
"github.com/gorilla/schema"
"github.com/pkg/errors"
"github.com/spf13/afero"
"go.uber.org/zap"
"golang.org/x/oauth2"
"k8s.io/api/core/v1"
"k8s.io/client-go/tools/clientcmd"
"k8s.io/client-go/tools/clientcmd/api"
)
Expand All @@ -23,6 +27,9 @@ const (
// be redirected after authentication.
DefaultKubeCfgEndpoint = "ui"

// DefaultAPITokenMountPath is the default mount path for API tokens
DefaultAPITokenMountPath = "/var/run/secrets/kubernetes.io/serviceaccount"

schemeHTTP = "http"
schemeHTTPS = "https"

Expand Down Expand Up @@ -67,6 +74,8 @@ var (

decoder = schema.NewDecoder()

appFs = afero.NewOsFs()

approvalConsent = oauth2.SetAuthURLParam("prompt", "consent")
)

Expand Down Expand Up @@ -358,9 +367,26 @@ func populateUser(cfg *api.Config, p *extractor.OIDCAuthenticationParams) api.Co
},
},
}

for name, cluster := range cfg.Clusters {
// If the cluster definition does not come with certificate-authority-data nor
// certificate-authority, then check if kuberos has access to the cluster's CA
// certificate and include it when possible. Assume all errors are non-fatal.
if len(cluster.CertificateAuthorityData) == 0 && cluster.CertificateAuthority == "" {
caPath := filepath.Join(DefaultAPITokenMountPath, v1.ServiceAccountRootCAKey)
if caFile, err := appFs.Open(caPath); err == nil {
if caCert, err := ioutil.ReadAll(caFile); err == nil {
cluster.CertificateAuthorityData = caCert
}
} else {
fmt.Printf("Error: %+v\n", err)
}
}
c.Clusters[name] = cluster
c.Contexts[name] = &api.Context{Cluster: name, AuthInfo: p.Username}
c.Contexts[name] = &api.Context{
Cluster: name,
AuthInfo: p.Username,
}
}
return c
}
91 changes: 91 additions & 0 deletions kuberos_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import (

oidc "github.com/coreos/go-oidc"
"github.com/go-test/deep"
"github.com/spf13/afero"
"golang.org/x/oauth2"

"github.com/negz/kuberos/extractor"
Expand Down Expand Up @@ -83,6 +84,7 @@ func TestPopulateUser(t *testing.T) {
cases := []struct {
name string
cfg *api.Config
files map[string]string
params *extractor.OIDCAuthenticationParams
want api.Config
}{
Expand All @@ -94,6 +96,7 @@ func TestPopulateUser(t *testing.T) {
"b": &api.Cluster{Server: "https://example.net", CertificateAuthorityData: []byte("PAM")},
},
},
files: map[string]string{},
params: &extractor.OIDCAuthenticationParams{
Username: "[email protected]",
ClientID: "id",
Expand Down Expand Up @@ -136,6 +139,7 @@ func TestPopulateUser(t *testing.T) {
},
CurrentContext: "a",
},
files: map[string]string{},
params: &extractor.OIDCAuthenticationParams{
Username: "[email protected]",
ClientID: "id",
Expand Down Expand Up @@ -170,10 +174,97 @@ func TestPopulateUser(t *testing.T) {
CurrentContext: "a",
},
},
{
name: "SingleClusterWithCAOnDisk",
cfg: &api.Config{
Clusters: map[string]*api.Cluster{
"a": &api.Cluster{Server: "https://example.org"},
},
},
files: map[string]string{
"/var/run/secrets/kubernetes.io/serviceaccount/ca.crt": "PEM",
},
params: &extractor.OIDCAuthenticationParams{
Username: "[email protected]",
ClientID: "id",
ClientSecret: "secret",
IDToken: "token",
RefreshToken: "refresh",
IssuerURL: "https://example.org",
},
want: api.Config{
Clusters: map[string]*api.Cluster{
"a": &api.Cluster{Server: "https://example.org", CertificateAuthorityData: []byte("PEM")},
},
Contexts: map[string]*api.Context{
"a": &api.Context{AuthInfo: "[email protected]", Cluster: "a"},
},
AuthInfos: map[string]*api.AuthInfo{
"[email protected]": &api.AuthInfo{
AuthProvider: &api.AuthProviderConfig{
Name: templateAuthProvider,
Config: map[string]string{
templateOIDCClientID: "id",
templateOIDCClientSecret: "secret",
templateOIDCIDToken: "token",
templateOIDCRefreshToken: "refresh",
templateOIDCIssuer: "https://example.org",
},
},
},
},
},
},
{
name: "SingleClusterWithoutCA",
cfg: &api.Config{
Clusters: map[string]*api.Cluster{
"a": &api.Cluster{Server: "https://example.org"},
},
},
files: map[string]string{},
params: &extractor.OIDCAuthenticationParams{
Username: "[email protected]",
ClientID: "id",
ClientSecret: "secret",
IDToken: "token",
RefreshToken: "refresh",
IssuerURL: "https://example.org",
},
want: api.Config{
Clusters: map[string]*api.Cluster{
"a": &api.Cluster{Server: "https://example.org"},
},
Contexts: map[string]*api.Context{
"a": &api.Context{AuthInfo: "[email protected]", Cluster: "a"},
},
AuthInfos: map[string]*api.AuthInfo{
"[email protected]": &api.AuthInfo{
AuthProvider: &api.AuthProviderConfig{
Name: templateAuthProvider,
Config: map[string]string{
templateOIDCClientID: "id",
templateOIDCClientSecret: "secret",
templateOIDCIDToken: "token",
templateOIDCRefreshToken: "refresh",
templateOIDCIssuer: "https://example.org",
},
},
},
},
},
},
}

for _, tt := range cases {
t.Run(tt.name, func(t *testing.T) {
appFs = afero.NewMemMapFs()
for filename, content := range tt.files {
if err := afero.WriteFile(appFs, filename, []byte(content), 0644); err != nil {
t.Errorf("error writing file %q: %v", filename, err)
}
}

got := populateUser(tt.cfg, tt.params)
if diff := deep.Equal(got, tt.want); diff != nil {
t.Errorf("populateUser(...): got != want: %v", diff)
Expand Down