Define credential IDs for X.509 certificates

This commit expands the existing credential ID concept to cover X.509
certificates.  We use the certificate's signature as the credential ID,
since this safe and unique.
This commit is contained in:
Taahir Ahmed 2024-06-21 16:21:35 -07:00
parent b510f785e6
commit 2ad2bd8907
5 changed files with 97 additions and 62 deletions

View File

@ -17,6 +17,7 @@ limitations under the License.
package x509 package x509
import ( import (
"crypto/sha256"
"crypto/x509" "crypto/x509"
"crypto/x509/pkix" "crypto/x509/pkix"
"encoding/hex" "encoding/hex"
@ -276,10 +277,17 @@ var CommonNameUserConversion = UserConversionFunc(func(chain []*x509.Certificate
if len(chain[0].Subject.CommonName) == 0 { if len(chain[0].Subject.CommonName) == 0 {
return nil, false, nil return nil, false, nil
} }
fp := sha256.Sum256(chain[0].Raw)
id := "X509SHA256=" + hex.EncodeToString(fp[:])
return &authenticator.Response{ return &authenticator.Response{
User: &user.DefaultInfo{ User: &user.DefaultInfo{
Name: chain[0].Subject.CommonName, Name: chain[0].Subject.CommonName,
Groups: chain[0].Subject.Organization, Groups: chain[0].Subject.Organization,
Extra: map[string][]string{
user.CredentialIDKey: {id},
},
}, },
}, true, nil }, true, nil
}) })

View File

@ -23,11 +23,11 @@ import (
"errors" "errors"
"io/ioutil" "io/ioutil"
"net/http" "net/http"
"reflect"
"sort" "sort"
"testing" "testing"
"time" "time"
"github.com/google/go-cmp/cmp"
"k8s.io/apimachinery/pkg/util/sets" "k8s.io/apimachinery/pkg/util/sets"
"k8s.io/apiserver/pkg/authentication/authenticator" "k8s.io/apiserver/pkg/authentication/authenticator"
"k8s.io/apiserver/pkg/authentication/user" "k8s.io/apiserver/pkg/authentication/user"
@ -581,9 +581,8 @@ func TestX509(t *testing.T) {
Opts x509.VerifyOptions Opts x509.VerifyOptions
User UserConversion User UserConversion
ExpectUserName string
ExpectGroups []string
ExpectOK bool ExpectOK bool
ExpectResponse *authenticator.Response
ExpectErr bool ExpectErr bool
}{ }{
"non-tls": { "non-tls": {
@ -618,9 +617,16 @@ func TestX509(t *testing.T) {
Certs: getCerts(t, serverCert), Certs: getCerts(t, serverCert),
User: CommonNameUserConversion, User: CommonNameUserConversion,
ExpectUserName: "127.0.0.1",
ExpectGroups: []string{"My Org"},
ExpectOK: true, ExpectOK: true,
ExpectResponse: &authenticator.Response{
User: &user.DefaultInfo{
Name: "127.0.0.1",
Groups: []string{"My Org"},
Extra: map[string][]string{
user.CredentialIDKey: {"X509SHA256=92209d1e0dd36a018f244f5e1b88e2d47b049e9cfcd4b7c87c65875866872230"},
},
},
},
ExpectErr: false, ExpectErr: false,
}, },
@ -629,9 +635,16 @@ func TestX509(t *testing.T) {
Certs: getCerts(t, clientCNCert), Certs: getCerts(t, clientCNCert),
User: CommonNameUserConversion, User: CommonNameUserConversion,
ExpectUserName: "client_cn",
ExpectGroups: []string{"My Org"},
ExpectOK: true, ExpectOK: true,
ExpectResponse: &authenticator.Response{
User: &user.DefaultInfo{
Name: "client_cn",
Groups: []string{"My Org"},
Extra: map[string][]string{
user.CredentialIDKey: {"X509SHA256=dd0a6a295055fa94455c522b0d54ef0499186f454a7cf978b8b346dc35b254f7"},
},
},
},
ExpectErr: false, ExpectErr: false,
}, },
"ca with multiple organizations": { "ca with multiple organizations": {
@ -641,9 +654,16 @@ func TestX509(t *testing.T) {
Certs: getCerts(t, caWithGroups), Certs: getCerts(t, caWithGroups),
User: CommonNameUserConversion, User: CommonNameUserConversion,
ExpectUserName: "ROOT CA WITH GROUPS",
ExpectGroups: []string{"My Org", "My Org 1", "My Org 2"},
ExpectOK: true, ExpectOK: true,
ExpectResponse: &authenticator.Response{
User: &user.DefaultInfo{
Name: "ROOT CA WITH GROUPS",
Groups: []string{"My Org", "My Org 1", "My Org 2"},
Extra: map[string][]string{
user.CredentialIDKey: {"X509SHA256=6f337bb6576b6f942bd5ac5256f621e352aa7b34d971bda9b8f8981f51bba456"},
},
},
},
ExpectErr: false, ExpectErr: false,
}, },
@ -664,8 +684,12 @@ func TestX509(t *testing.T) {
return &authenticator.Response{User: &user.DefaultInfo{Name: "custom"}}, true, nil return &authenticator.Response{User: &user.DefaultInfo{Name: "custom"}}, true, nil
}), }),
ExpectUserName: "custom",
ExpectOK: true, ExpectOK: true,
ExpectResponse: &authenticator.Response{
User: &user.DefaultInfo{
Name: "custom",
},
},
ExpectErr: false, ExpectErr: false,
}, },
@ -697,8 +721,15 @@ func TestX509(t *testing.T) {
Certs: getCertsFromFile(t, "client-valid", "intermediate"), Certs: getCertsFromFile(t, "client-valid", "intermediate"),
User: CommonNameUserConversion, User: CommonNameUserConversion,
ExpectUserName: "My Client",
ExpectOK: true, ExpectOK: true,
ExpectResponse: &authenticator.Response{
User: &user.DefaultInfo{
Name: "My Client",
Extra: map[string][]string{
user.CredentialIDKey: {"X509SHA256=794b0529fd1a72d55d52d98be9bab5b822d16f9ae86c4373fa7beee3cafe8582"},
},
},
},
ExpectErr: false, ExpectErr: false,
}, },
"multi-level, expired": { "multi-level, expired": {
@ -712,6 +743,7 @@ func TestX509(t *testing.T) {
} }
for k, testCase := range testCases { for k, testCase := range testCases {
t.Run(k, func(t *testing.T) {
req, _ := http.NewRequest("GET", "/", nil) req, _ := http.NewRequest("GET", "/", nil)
if !testCase.Insecure { if !testCase.Insecure {
req.TLS = &tls.ConnectionState{PeerCertificates: testCase.Certs} req.TLS = &tls.ConnectionState{PeerCertificates: testCase.Certs}
@ -723,31 +755,24 @@ func TestX509(t *testing.T) {
resp, ok, err := a.AuthenticateRequest(req) resp, ok, err := a.AuthenticateRequest(req)
if testCase.ExpectErr && err == nil { if testCase.ExpectErr && err == nil {
t.Errorf("%s: Expected error, got none", k) t.Fatalf("Expected error, got none")
continue
} }
if !testCase.ExpectErr && err != nil { if !testCase.ExpectErr && err != nil {
t.Errorf("%s: Got unexpected error: %v", k, err) t.Fatalf("Got unexpected error: %v", err)
continue
} }
if testCase.ExpectOK != ok { if testCase.ExpectOK != ok {
t.Errorf("%s: Expected ok=%v, got %v", k, testCase.ExpectOK, ok) t.Fatalf("Expected ok=%v, got %v", testCase.ExpectOK, ok)
continue
} }
if testCase.ExpectOK { if testCase.ExpectOK {
if testCase.ExpectUserName != resp.User.GetName() { sort.Strings(testCase.ExpectResponse.User.GetGroups())
t.Errorf("%s: Expected user.name=%v, got %v", k, testCase.ExpectUserName, resp.User.GetName()) sort.Strings(resp.User.GetGroups())
} if diff := cmp.Diff(testCase.ExpectResponse, resp); diff != "" {
t.Errorf("Bad response; diff (-want +got)\n%s", diff)
groups := resp.User.GetGroups()
sort.Strings(testCase.ExpectGroups)
sort.Strings(groups)
if !reflect.DeepEqual(testCase.ExpectGroups, groups) {
t.Errorf("%s: Expected user.groups=%v, got %v", k, testCase.ExpectGroups, groups)
} }
} }
})
} }
} }

View File

@ -36,9 +36,6 @@ const (
ServiceAccountUsernameSeparator = ":" ServiceAccountUsernameSeparator = ":"
ServiceAccountGroupPrefix = "system:serviceaccounts:" ServiceAccountGroupPrefix = "system:serviceaccounts:"
AllServiceAccountsGroup = "system:serviceaccounts" AllServiceAccountsGroup = "system:serviceaccounts"
// CredentialIDKey is the key used in a user's "extra" to specify the unique
// identifier for this identity document).
CredentialIDKey = "authentication.kubernetes.io/credential-id"
// IssuedCredentialIDAuditAnnotationKey is the annotation key used in the audit event that is persisted to the // IssuedCredentialIDAuditAnnotationKey is the annotation key used in the audit event that is persisted to the
// '/token' endpoint for service accounts. // '/token' endpoint for service accounts.
// This annotation indicates the generated credential identifier for the service account token being issued. // This annotation indicates the generated credential identifier for the service account token being issued.
@ -156,7 +153,7 @@ func (sa *ServiceAccountInfo) UserInfo() user.Info {
if info.Extra == nil { if info.Extra == nil {
info.Extra = make(map[string][]string) info.Extra = make(map[string][]string)
} }
info.Extra[CredentialIDKey] = []string{sa.CredentialID} info.Extra[user.CredentialIDKey] = []string{sa.CredentialID}
} }
if sa.NodeName != "" { if sa.NodeName != "" {
if info.Extra == nil { if info.Extra == nil {

View File

@ -66,8 +66,8 @@ func (i *DefaultInfo) GetExtra() map[string][]string {
return i.Extra return i.Extra
} }
// well-known user and group names
const ( const (
// well-known user and group names
SystemPrivilegedGroup = "system:masters" SystemPrivilegedGroup = "system:masters"
NodesGroup = "system:nodes" NodesGroup = "system:nodes"
MonitoringGroup = "system:monitoring" MonitoringGroup = "system:monitoring"
@ -81,4 +81,8 @@ const (
KubeProxy = "system:kube-proxy" KubeProxy = "system:kube-proxy"
KubeControllerManager = "system:kube-controller-manager" KubeControllerManager = "system:kube-controller-manager"
KubeScheduler = "system:kube-scheduler" KubeScheduler = "system:kube-scheduler"
// CredentialIDKey is the key used in a user's "extra" to specify the unique
// identifier for this identity document).
CredentialIDKey = "authentication.kubernetes.io/credential-id"
) )

View File

@ -41,6 +41,7 @@ import (
"k8s.io/apimachinery/pkg/types" "k8s.io/apimachinery/pkg/types"
"k8s.io/apiserver/pkg/authentication/authenticator" "k8s.io/apiserver/pkg/authentication/authenticator"
apiserverserviceaccount "k8s.io/apiserver/pkg/authentication/serviceaccount" apiserverserviceaccount "k8s.io/apiserver/pkg/authentication/serviceaccount"
"k8s.io/apiserver/pkg/authentication/user"
utilfeature "k8s.io/apiserver/pkg/util/feature" utilfeature "k8s.io/apiserver/pkg/util/feature"
clientset "k8s.io/client-go/kubernetes" clientset "k8s.io/client-go/kubernetes"
"k8s.io/client-go/kubernetes/scheme" "k8s.io/client-go/kubernetes/scheme"
@ -237,7 +238,7 @@ func TestServiceAccountTokenCreate(t *testing.T) {
info := doTokenReview(t, cs, treq, false) info := doTokenReview(t, cs, treq, false)
// we are not testing the credential-id feature, so delete this value from the returned extra info map // we are not testing the credential-id feature, so delete this value from the returned extra info map
if info.Extra != nil { if info.Extra != nil {
delete(info.Extra, apiserverserviceaccount.CredentialIDKey) delete(info.Extra, user.CredentialIDKey)
} }
if len(info.Extra) > 0 { if len(info.Extra) > 0 {
t.Fatalf("expected Extra to be empty but got: %#v", info.Extra) t.Fatalf("expected Extra to be empty but got: %#v", info.Extra)
@ -309,7 +310,7 @@ func TestServiceAccountTokenCreate(t *testing.T) {
info := doTokenReview(t, cs, treq, false) info := doTokenReview(t, cs, treq, false)
// we are not testing the credential-id feature, so delete this value from the returned extra info map // we are not testing the credential-id feature, so delete this value from the returned extra info map
delete(info.Extra, apiserverserviceaccount.CredentialIDKey) delete(info.Extra, user.CredentialIDKey)
if len(info.Extra) != 2 { if len(info.Extra) != 2 {
t.Fatalf("expected Extra have length of 2 but was length %d: %#v", len(info.Extra), info.Extra) t.Fatalf("expected Extra have length of 2 but was length %d: %#v", len(info.Extra), info.Extra)
} }
@ -405,7 +406,7 @@ func TestServiceAccountTokenCreate(t *testing.T) {
info := doTokenReview(t, cs, treq, false) info := doTokenReview(t, cs, treq, false)
// we are not testing the credential-id feature, so delete this value from the returned extra info map // we are not testing the credential-id feature, so delete this value from the returned extra info map
delete(info.Extra, apiserverserviceaccount.CredentialIDKey) delete(info.Extra, user.CredentialIDKey)
if len(info.Extra) != len(expectedExtraValues) { if len(info.Extra) != len(expectedExtraValues) {
t.Fatalf("expected Extra have length of %d but was length %d: %#v", len(expectedExtraValues), len(info.Extra), info.Extra) t.Fatalf("expected Extra have length of %d but was length %d: %#v", len(expectedExtraValues), len(info.Extra), info.Extra)
} }