Skip to content

Commit

Permalink
Ensure valid entity alias names created for projected volume tokens. (#…
Browse files Browse the repository at this point in the history
  • Loading branch information
benashz authored Feb 17, 2022
1 parent c9fa6ac commit a0cae68
Show file tree
Hide file tree
Showing 4 changed files with 320 additions and 3 deletions.
2 changes: 2 additions & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,14 @@ go 1.12
require (
github.com/briankassouf/jose v0.9.2-0.20180619214549-d2569464773f
github.com/go-test/deep v1.0.8
github.com/golang-jwt/jwt v3.2.2+incompatible
github.com/hashicorp/errwrap v1.1.0
github.com/hashicorp/go-cleanhttp v0.5.2
github.com/hashicorp/go-hclog v1.0.0
github.com/hashicorp/go-multierror v1.1.1
github.com/hashicorp/go-secure-stdlib/strutil v0.1.1
github.com/hashicorp/go-sockaddr v1.0.2
github.com/hashicorp/go-uuid v1.0.2
github.com/hashicorp/vault/api v1.2.0
github.com/hashicorp/vault/sdk v0.2.1
github.com/hashicorp/yamux v0.0.0-20181012175058-2f1d1f20f75d // indirect
Expand Down
2 changes: 2 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,8 @@ github.com/gogo/protobuf v1.2.1/go.mod h1:hp+jE20tsWTFYpLwKvXlhS1hjn+gTNwPg2I6zV
github.com/gogo/protobuf v1.3.1/go.mod h1:SlYgWuQ5SjCEi6WLHjHCa1yvBfUnHcTbrrZtXPKa29o=
github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q=
github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q=
github.com/golang-jwt/jwt v3.2.2+incompatible h1:IfV12K8xAKAnZqdXVzCZ+TOjboZ2keLg81eXfW3O+oY=
github.com/golang-jwt/jwt v3.2.2+incompatible/go.mod h1:8pz2t5EyA70fFQQSrl6XZXzqecmYZeUEB8OUGHkxJ+I=
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
github.com/golang/groupcache v0.0.0-20160516000752-02826c3e7903/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
Expand Down
8 changes: 6 additions & 2 deletions path_login.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ import (
"github.com/briankassouf/jose/jws"
"github.com/briankassouf/jose/jwt"
"github.com/hashicorp/errwrap"
multierror "github.com/hashicorp/go-multierror"
"github.com/hashicorp/go-multierror"
"github.com/hashicorp/go-secure-stdlib/strutil"
"github.com/hashicorp/vault/sdk/framework"
"github.com/hashicorp/vault/sdk/helper/cidrutil"
Expand Down Expand Up @@ -160,7 +160,11 @@ func (b *kubeAuthBackend) getAliasName(role *roleStorageEntry, serviceAccount *s
}
return uid, nil
case aliasNameSourceSAName:
return fmt.Sprintf("%s/%s", serviceAccount.Namespace, serviceAccount.Name), nil
ns, name := serviceAccount.namespace(), serviceAccount.name()
if ns == "" || name == "" {
return "", fmt.Errorf("service account namespace and name must be set")
}
return fmt.Sprintf("%s/%s", ns, name), nil
default:
return "", fmt.Errorf("unknown alias_name_source %q", role.AliasNameSource)
}
Expand Down
311 changes: 310 additions & 1 deletion path_login_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,26 @@ package kubeauth

import (
"context"
"crypto/rand"
"crypto/rsa"
"errors"
"fmt"
"reflect"
"testing"
"time"

"github.com/briankassouf/jose/jws"
// TODO: using github.com/golang-jwt/jwt for tests only,
// as a part of moving away from jose we should consider standardizing
// on a single JWT library for tests and runtime uses.
"github.com/golang-jwt/jwt"
"github.com/hashicorp/errwrap"
multierror "github.com/hashicorp/go-multierror"
"github.com/hashicorp/go-multierror"
"github.com/hashicorp/go-uuid"
"github.com/hashicorp/vault/sdk/logical"
"github.com/mitchellh/mapstructure"
v1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/types"
)

var (
Expand Down Expand Up @@ -1088,3 +1100,300 @@ WKDrXOZIZAMPHZtt2MojxdGpPxiBSVODn6hw8n4hGBWuH7UABU+2h2kZI0ctxWaX
UIX4hSHyjlKYDGEezrUP1mm7AX5pN1qrjtxasTSPPX8nZY/3HtM77n4PfYEwCrew
rwIDAQAB
-----END PUBLIC KEY-----`

func Test_kubeAuthBackend_getAliasName(t *testing.T) {
expectedErr := fmt.Errorf("service account namespace and name must be set")
issuerDefault := "kubernetes/serviceaccount"
issuerProjected := "https://kubernetes.default.svc.cluster.local"

tests := []struct {
name string
role *roleStorageEntry
signRequest *jwtSignTestRequest
want string
wantErr bool
}{
{
name: "default",
role: &roleStorageEntry{
AliasNameSource: aliasNameSourceDefault,
},
signRequest: &jwtSignTestRequest{
issuer: issuerDefault,
ns: "default",
sa: "sa",
uid: testUID,
projected: false,
},
want: testUID,
wantErr: false,
},
{
name: "default-sa-uid",
role: &roleStorageEntry{
AliasNameSource: aliasNameSourceSAUid,
},
signRequest: &jwtSignTestRequest{
issuer: issuerDefault,
ns: "default",
sa: "sa",
uid: testUID,
projected: false,
},
want: testUID,
wantErr: false,
},
{
name: "default-sa-name",
role: &roleStorageEntry{
AliasNameSource: aliasNameSourceSAName,
},
signRequest: &jwtSignTestRequest{
issuer: issuerDefault,
ns: "default",
sa: "sa",
projected: false,
},
want: fmt.Sprintf("%s/%s", "default", "sa"),
wantErr: false,
},
{
name: "invalid-default-empty-ns",
role: &roleStorageEntry{
AliasNameSource: aliasNameSourceSAName,
},
signRequest: &jwtSignTestRequest{
issuer: issuerProjected,
ns: "",
sa: "sa2",
projected: false,
},
want: "",
wantErr: true,
},
{
name: "invalid-default-empty-sa",
role: &roleStorageEntry{
AliasNameSource: aliasNameSourceSAName,
},
signRequest: &jwtSignTestRequest{
issuer: issuerProjected,
ns: "default",
sa: "",
projected: false,
},
want: "",
wantErr: true,
},
{
name: "projected",
role: &roleStorageEntry{
AliasNameSource: aliasNameSourceDefault,
},
signRequest: &jwtSignTestRequest{
issuer: issuerProjected,
ns: "default",
sa: "sa",
uid: testProjectedUID,
projected: true,
},
want: testProjectedUID,
wantErr: false,
},
{
name: "projected-sa-uid",
role: &roleStorageEntry{
AliasNameSource: aliasNameSourceSAUid,
},
signRequest: &jwtSignTestRequest{
issuer: issuerProjected,
ns: "default",
sa: "sa",
uid: testProjectedUID,
projected: true,
},
want: testProjectedUID,
wantErr: false,
},
{
name: "projected-sa-name",
role: &roleStorageEntry{
AliasNameSource: aliasNameSourceSAName,
},
signRequest: &jwtSignTestRequest{
issuer: issuerProjected,
ns: "ns1",
sa: "sa",
projected: true,
},
want: fmt.Sprintf("%s/%s", "ns1", "sa"),
wantErr: false,
},
{
name: "invalid-projected-empty-ns",
role: &roleStorageEntry{
AliasNameSource: aliasNameSourceSAName,
},
signRequest: &jwtSignTestRequest{
issuer: issuerProjected,
ns: "",
sa: "sa2",
projected: true,
},
want: "",
wantErr: true,
},
{
name: "invalid-projected-empty-sa",
role: &roleStorageEntry{
AliasNameSource: aliasNameSourceSAName,
},
signRequest: &jwtSignTestRequest{
issuer: issuerProjected,
ns: "default",
sa: "",
projected: true,
},
want: "",
wantErr: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
b := &kubeAuthBackend{}

s, err := signTestJWTRequest(tt.signRequest)
if err != nil {
t.Fatal(err)
}

parsedJWT, err := jws.ParseJWT([]byte(s))
if err != nil {
t.Fatal(err)
}

sa := &serviceAccount{}
if err := mapstructure.Decode(parsedJWT.Claims(), sa); err != nil {
t.Fatal(err)
}

got, err := b.getAliasName(tt.role, sa)

if tt.wantErr {
if err == nil {
t.Errorf("getAliasName() error = %v, wantErr %v", err, tt.wantErr)
return
}
if !reflect.DeepEqual(expectedErr, err) {
t.Errorf("getAliasName() expected error = %v, actual %v", expectedErr, err)
}
}

if got != tt.want {
t.Errorf("getAliasName() got = %v, want %v", got, tt.want)
}
})
}
}

type jwtSignTestRequest struct {
ns string
sa string
uid string
projected bool
issuer string
expired bool
}

func (r *jwtSignTestRequest) getUID() string {
var uid string
if r.uid == "" {
uid, _ = uuid.GenerateUUID()
r.uid = uid
}

return r.uid
}

func signTestJWTRequest(req *jwtSignTestRequest) (string, error) {
var claims jwt.Claims
if req.projected {
claims = projectedJWTTestClaims(req)
} else {
claims = defaultJWTTestClaims(req)
}

return signTestJWT(claims)
}

func jwtStandardTestClaims(req *jwtSignTestRequest) jwt.StandardClaims {
now := time.Now()
var horizon int64 = 86400
if req.expired {
horizon = horizon * -1
}
return jwt.StandardClaims{
IssuedAt: now.Unix(),
ExpiresAt: now.Unix() + horizon,
Issuer: req.issuer,
}
}

func projectedJWTTestClaims(req *jwtSignTestRequest) jwt.Claims {
type testToken struct {
Namespace string `json:"namespace"`
Pod *v1.ObjectMeta `json:"pod"`
ServiceAccount *v1.ObjectMeta `json:"serviceaccount"`
}

type Claims struct {
Audiences []string `json:"aud"`
Token *testToken `json:"kubernetes.io"`
jwt.StandardClaims
}

uid := types.UID(req.getUID())
return &Claims{
Audiences: []string{"baz"},
Token: &testToken{
Namespace: req.ns,
Pod: &v1.ObjectMeta{
Name: "pod",
UID: uid,
},
ServiceAccount: &v1.ObjectMeta{
Name: req.sa,
UID: uid,
},
},
StandardClaims: jwtStandardTestClaims(req),
}
}

func defaultJWTTestClaims(req *jwtSignTestRequest) jwt.Claims {
type Claims struct {
Namespace string `json:"kubernetes.io/serviceaccount/namespace"`
SecretName string `json:"kubernetes.io/serviceaccount/secret.name"`
ServiceAccountName string `json:"kubernetes.io/serviceaccount/service-account.name"`
UID string `json:"kubernetes.io/serviceaccount/service-account.uid"`
Sub string `json:"sub"`
jwt.StandardClaims
}

return &Claims{
Namespace: req.ns,
ServiceAccountName: req.sa,
UID: req.getUID(),
StandardClaims: jwtStandardTestClaims(req),
}
}

func signTestJWT(claims jwt.Claims) (string, error) {
pkey, err := rsa.GenerateKey(rand.Reader, 2048)
if err != nil {
return "", err
}
token := jwt.NewWithClaims(jwt.SigningMethodRS256, claims)

return token.SignedString(pkey)
}

0 comments on commit a0cae68

Please sign in to comment.