mirror of
https://github.com/k3s-io/kubernetes.git
synced 2025-07-22 03:11:40 +00:00
Merge pull request #36816 from deads2k/api-43-front-proxy
Automatic merge from submit-queue plumb in front proxy group header Builds on https://github.com/kubernetes/kubernetes/pull/36662 and https://github.com/kubernetes/kubernetes/pull/36774, so only the last commit is unique. This completes the plumbing for front proxy header information and makes it possible to add just the front proxy header authenticator. WIP because I'm going to assess it in use downstream.
This commit is contained in:
commit
71182d826d
@ -496,6 +496,8 @@ report-dir
|
||||
report-prefix
|
||||
requestheader-allowed-names
|
||||
requestheader-client-ca-file
|
||||
requestheader-extra-headers-prefix
|
||||
requestheader-group-headers
|
||||
requestheader-username-headers
|
||||
require-kubeconfig
|
||||
required-contexts
|
||||
|
@ -43,6 +43,11 @@ import (
|
||||
type RequestHeaderConfig struct {
|
||||
// UsernameHeaders are the headers to check (in order, case-insensitively) for an identity. The first header with a value wins.
|
||||
UsernameHeaders []string
|
||||
// GroupHeaders are the headers to check (case-insensitively) for a group names. All values will be used.
|
||||
GroupHeaders []string
|
||||
// ExtraHeaderPrefixes are the head prefixes to check (case-insentively) for filling in
|
||||
// the user.Info.Extra. All values of all matching headers will be added.
|
||||
ExtraHeaderPrefixes []string
|
||||
// ClientCA points to CA bundle file which is used verify the identity of the front proxy
|
||||
ClientCA string
|
||||
// AllowedClientNames is a list of common names that may be presented by the authenticating front proxy. Empty means: accept any.
|
||||
@ -88,6 +93,8 @@ func New(config AuthenticatorConfig) (authenticator.Request, *spec.SecurityDefin
|
||||
config.RequestHeaderConfig.ClientCA,
|
||||
config.RequestHeaderConfig.AllowedClientNames,
|
||||
config.RequestHeaderConfig.UsernameHeaders,
|
||||
config.RequestHeaderConfig.GroupHeaders,
|
||||
config.RequestHeaderConfig.ExtraHeaderPrefixes,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
|
@ -63,12 +63,6 @@ type PasswordFileAuthenticationOptions struct {
|
||||
BasicAuthFile string
|
||||
}
|
||||
|
||||
type RequestHeaderAuthenticationOptions struct {
|
||||
UsernameHeaders []string
|
||||
ClientCAFile string
|
||||
AllowedNames []string
|
||||
}
|
||||
|
||||
type ServiceAccountAuthenticationOptions struct {
|
||||
KeyFiles []string
|
||||
Lookup bool
|
||||
@ -206,17 +200,7 @@ func (s *BuiltInAuthenticationOptions) AddFlags(fs *pflag.FlagSet) {
|
||||
}
|
||||
|
||||
if s.RequestHeader != nil {
|
||||
fs.StringSliceVar(&s.RequestHeader.UsernameHeaders, "requestheader-username-headers", s.RequestHeader.UsernameHeaders, ""+
|
||||
"List of request headers to inspect for usernames. X-Remote-User is common.")
|
||||
|
||||
fs.StringVar(&s.RequestHeader.ClientCAFile, "requestheader-client-ca-file", s.RequestHeader.ClientCAFile, ""+
|
||||
"Root certificate bundle to use to verify client certificates on incoming requests "+
|
||||
"before trusting usernames in headers specified by --requestheader-username-headers")
|
||||
|
||||
fs.StringSliceVar(&s.RequestHeader.AllowedNames, "requestheader-allowed-names", s.RequestHeader.AllowedNames, ""+
|
||||
"List of client certificate common names to allow to provide usernames in headers "+
|
||||
"specified by --requestheader-username-headers. If empty, any client certificate validated "+
|
||||
"by the authorities in --requestheader-client-ca-file is allowed.")
|
||||
s.RequestHeader.AddFlags(fs)
|
||||
}
|
||||
|
||||
if s.ServiceAccounts != nil {
|
||||
@ -275,7 +259,7 @@ func (s *BuiltInAuthenticationOptions) ToAuthenticationConfig(clientCAFile strin
|
||||
}
|
||||
|
||||
if s.RequestHeader != nil {
|
||||
ret.RequestHeaderConfig = s.RequestHeader.AuthenticationRequestHeaderConfig()
|
||||
ret.RequestHeaderConfig = s.RequestHeader.ToAuthenticationRequestHeaderConfig()
|
||||
}
|
||||
|
||||
if s.ServiceAccounts != nil {
|
||||
@ -295,15 +279,45 @@ func (s *BuiltInAuthenticationOptions) ToAuthenticationConfig(clientCAFile strin
|
||||
return ret
|
||||
}
|
||||
|
||||
// AuthenticationRequestHeaderConfig returns an authenticator config object for these options
|
||||
// if necessary. nil otherwise.
|
||||
func (s *RequestHeaderAuthenticationOptions) AuthenticationRequestHeaderConfig() *authenticator.RequestHeaderConfig {
|
||||
type RequestHeaderAuthenticationOptions struct {
|
||||
UsernameHeaders []string
|
||||
GroupHeaders []string
|
||||
ExtraHeaderPrefixes []string
|
||||
ClientCAFile string
|
||||
AllowedNames []string
|
||||
}
|
||||
|
||||
func (s *RequestHeaderAuthenticationOptions) AddFlags(fs *pflag.FlagSet) {
|
||||
fs.StringSliceVar(&s.UsernameHeaders, "requestheader-username-headers", s.UsernameHeaders, ""+
|
||||
"List of request headers to inspect for usernames. X-Remote-User is common.")
|
||||
|
||||
fs.StringSliceVar(&s.GroupHeaders, "requestheader-group-headers", s.GroupHeaders, ""+
|
||||
"List of request headers to inspect for groups. X-Remote-Group is suggested.")
|
||||
|
||||
fs.StringSliceVar(&s.ExtraHeaderPrefixes, "requestheader-extra-headers-prefix", s.ExtraHeaderPrefixes, ""+
|
||||
"List of request header prefixes to inspect. X-Remote-Extra- is suggested.")
|
||||
|
||||
fs.StringVar(&s.ClientCAFile, "requestheader-client-ca-file", s.ClientCAFile, ""+
|
||||
"Root certificate bundle to use to verify client certificates on incoming requests "+
|
||||
"before trusting usernames in headers specified by --requestheader-username-headers")
|
||||
|
||||
fs.StringSliceVar(&s.AllowedNames, "requestheader-allowed-names", s.AllowedNames, ""+
|
||||
"List of client certificate common names to allow to provide usernames in headers "+
|
||||
"specified by --requestheader-username-headers. If empty, any client certificate validated "+
|
||||
"by the authorities in --requestheader-client-ca-file is allowed.")
|
||||
}
|
||||
|
||||
// ToAuthenticationRequestHeaderConfig returns a RequestHeaderConfig config object for these options
|
||||
// if necessary, nil otherwise.
|
||||
func (s *RequestHeaderAuthenticationOptions) ToAuthenticationRequestHeaderConfig() *authenticator.RequestHeaderConfig {
|
||||
if len(s.UsernameHeaders) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
return &authenticator.RequestHeaderConfig{
|
||||
UsernameHeaders: s.UsernameHeaders,
|
||||
GroupHeaders: s.GroupHeaders,
|
||||
ExtraHeaderPrefixes: s.ExtraHeaderPrefixes,
|
||||
ClientCA: s.ClientCAFile,
|
||||
AllowedClientNames: s.AllowedNames,
|
||||
}
|
||||
|
@ -33,23 +33,51 @@ import (
|
||||
type requestHeaderAuthRequestHandler struct {
|
||||
// nameHeaders are the headers to check (in order, case-insensitively) for an identity. The first header with a value wins.
|
||||
nameHeaders []string
|
||||
|
||||
// groupHeaders are the headers to check (case-insensitively) for group membership. All values of all headers will be added.
|
||||
groupHeaders []string
|
||||
|
||||
// extraHeaderPrefixes are the head prefixes to check (case-insensitively) for filling in
|
||||
// the user.Info.Extra. All values of all matching headers will be added.
|
||||
extraHeaderPrefixes []string
|
||||
}
|
||||
|
||||
func New(nameHeaders []string) (authenticator.Request, error) {
|
||||
headers := []string{}
|
||||
for _, headerName := range nameHeaders {
|
||||
func New(nameHeaders []string, groupHeaders []string, extraHeaderPrefixes []string) (authenticator.Request, error) {
|
||||
trimmedNameHeaders, err := trimHeaders(nameHeaders...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
trimmedGroupHeaders, err := trimHeaders(groupHeaders...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
trimmedExtraHeaderPrefixes, err := trimHeaders(extraHeaderPrefixes...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &requestHeaderAuthRequestHandler{
|
||||
nameHeaders: trimmedNameHeaders,
|
||||
groupHeaders: trimmedGroupHeaders,
|
||||
extraHeaderPrefixes: trimmedExtraHeaderPrefixes,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func trimHeaders(headerNames ...string) ([]string, error) {
|
||||
ret := []string{}
|
||||
for _, headerName := range headerNames {
|
||||
trimmedHeader := strings.TrimSpace(headerName)
|
||||
if len(trimmedHeader) == 0 {
|
||||
return nil, fmt.Errorf("empty header %q", headerName)
|
||||
}
|
||||
headers = append(headers, trimmedHeader)
|
||||
ret = append(ret, trimmedHeader)
|
||||
}
|
||||
|
||||
return &requestHeaderAuthRequestHandler{nameHeaders: headers}, nil
|
||||
return ret, nil
|
||||
}
|
||||
|
||||
func NewSecure(clientCA string, proxyClientNames []string, nameHeaders []string) (authenticator.Request, error) {
|
||||
headerAuthenticator, err := New(nameHeaders)
|
||||
func NewSecure(clientCA string, proxyClientNames []string, nameHeaders []string, groupHeaders []string, extraHeaderPrefixes []string) (authenticator.Request, error) {
|
||||
headerAuthenticator, err := New(nameHeaders, groupHeaders, extraHeaderPrefixes)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@ -81,8 +109,27 @@ func (a *requestHeaderAuthRequestHandler) AuthenticateRequest(req *http.Request)
|
||||
if len(name) == 0 {
|
||||
return nil, false, nil
|
||||
}
|
||||
groups := allHeaderValues(req.Header, a.groupHeaders)
|
||||
extra := newExtra(req.Header, a.extraHeaderPrefixes)
|
||||
|
||||
return &user.DefaultInfo{Name: name}, true, nil
|
||||
// clear headers used for authentication
|
||||
for _, headerName := range a.nameHeaders {
|
||||
req.Header.Del(headerName)
|
||||
}
|
||||
for _, headerName := range a.groupHeaders {
|
||||
req.Header.Del(headerName)
|
||||
}
|
||||
for k := range extra {
|
||||
for _, prefix := range a.extraHeaderPrefixes {
|
||||
req.Header.Del(prefix + k)
|
||||
}
|
||||
}
|
||||
|
||||
return &user.DefaultInfo{
|
||||
Name: name,
|
||||
Groups: groups,
|
||||
Extra: extra,
|
||||
}, true, nil
|
||||
}
|
||||
|
||||
func headerValue(h http.Header, headerNames []string) string {
|
||||
@ -94,3 +141,38 @@ func headerValue(h http.Header, headerNames []string) string {
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func allHeaderValues(h http.Header, headerNames []string) []string {
|
||||
ret := []string{}
|
||||
for _, headerName := range headerNames {
|
||||
values, ok := h[headerName]
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
|
||||
for _, headerValue := range values {
|
||||
if len(headerValue) > 0 {
|
||||
ret = append(ret, headerValue)
|
||||
}
|
||||
}
|
||||
}
|
||||
return ret
|
||||
}
|
||||
|
||||
func newExtra(h http.Header, headerPrefixes []string) map[string][]string {
|
||||
ret := map[string][]string{}
|
||||
|
||||
// we have to iterate over prefixes first in order to have proper ordering inside the value slices
|
||||
for _, prefix := range headerPrefixes {
|
||||
for headerName, vv := range h {
|
||||
if !strings.HasPrefix(strings.ToLower(headerName), strings.ToLower(prefix)) {
|
||||
continue
|
||||
}
|
||||
|
||||
extraKey := strings.ToLower(headerName[len(prefix):])
|
||||
ret[extraKey] = append(ret[extraKey], vv...)
|
||||
}
|
||||
}
|
||||
|
||||
return ret
|
||||
}
|
||||
|
@ -27,29 +27,35 @@ import (
|
||||
func TestRequestHeader(t *testing.T) {
|
||||
testcases := map[string]struct {
|
||||
nameHeaders []string
|
||||
groupHeaders []string
|
||||
extraPrefixHeaders []string
|
||||
requestHeaders http.Header
|
||||
|
||||
expectedUser user.Info
|
||||
expectedOk bool
|
||||
}{
|
||||
"empty": {},
|
||||
"no match": {
|
||||
"user no match": {
|
||||
nameHeaders: []string{"X-Remote-User"},
|
||||
},
|
||||
"match": {
|
||||
"user match": {
|
||||
nameHeaders: []string{"X-Remote-User"},
|
||||
requestHeaders: http.Header{"X-Remote-User": {"Bob"}},
|
||||
expectedUser: &user.DefaultInfo{Name: "Bob"},
|
||||
expectedUser: &user.DefaultInfo{
|
||||
Name: "Bob",
|
||||
Groups: []string{},
|
||||
Extra: map[string][]string{},
|
||||
},
|
||||
expectedOk: true,
|
||||
},
|
||||
"exact match": {
|
||||
"user exact match": {
|
||||
nameHeaders: []string{"X-Remote-User"},
|
||||
requestHeaders: http.Header{
|
||||
"Prefixed-X-Remote-User-With-Suffix": {"Bob"},
|
||||
"X-Remote-User-With-Suffix": {"Bob"},
|
||||
},
|
||||
},
|
||||
"first match": {
|
||||
"user first match": {
|
||||
nameHeaders: []string{
|
||||
"X-Remote-User",
|
||||
"A-Second-X-Remote-User",
|
||||
@ -59,19 +65,83 @@ func TestRequestHeader(t *testing.T) {
|
||||
"X-Remote-User": {"", "First header, second value"},
|
||||
"A-Second-X-Remote-User": {"Second header, first value", "Second header, second value"},
|
||||
"Another-X-Remote-User": {"Third header, first value"}},
|
||||
expectedUser: &user.DefaultInfo{Name: "Second header, first value"},
|
||||
expectedUser: &user.DefaultInfo{
|
||||
Name: "Second header, first value",
|
||||
Groups: []string{},
|
||||
Extra: map[string][]string{},
|
||||
},
|
||||
expectedOk: true,
|
||||
},
|
||||
"case-insensitive": {
|
||||
"user case-insensitive": {
|
||||
nameHeaders: []string{"x-REMOTE-user"}, // configured headers can be case-insensitive
|
||||
requestHeaders: http.Header{"X-Remote-User": {"Bob"}}, // the parsed headers are normalized by the http package
|
||||
expectedUser: &user.DefaultInfo{Name: "Bob"},
|
||||
expectedUser: &user.DefaultInfo{
|
||||
Name: "Bob",
|
||||
Groups: []string{},
|
||||
Extra: map[string][]string{},
|
||||
},
|
||||
expectedOk: true,
|
||||
},
|
||||
|
||||
"groups none": {
|
||||
nameHeaders: []string{"X-Remote-User"},
|
||||
groupHeaders: []string{"X-Remote-Group"},
|
||||
requestHeaders: http.Header{
|
||||
"X-Remote-User": {"Bob"},
|
||||
},
|
||||
expectedUser: &user.DefaultInfo{
|
||||
Name: "Bob",
|
||||
Groups: []string{},
|
||||
Extra: map[string][]string{},
|
||||
},
|
||||
expectedOk: true,
|
||||
},
|
||||
"groups all matches": {
|
||||
nameHeaders: []string{"X-Remote-User"},
|
||||
groupHeaders: []string{"X-Remote-Group-1", "X-Remote-Group-2"},
|
||||
requestHeaders: http.Header{
|
||||
"X-Remote-User": {"Bob"},
|
||||
"X-Remote-Group-1": {"one-a", "one-b"},
|
||||
"X-Remote-Group-2": {"two-a", "two-b"},
|
||||
},
|
||||
expectedUser: &user.DefaultInfo{
|
||||
Name: "Bob",
|
||||
Groups: []string{"one-a", "one-b", "two-a", "two-b"},
|
||||
Extra: map[string][]string{},
|
||||
},
|
||||
expectedOk: true,
|
||||
},
|
||||
|
||||
"extra prefix matches case-insensitive": {
|
||||
nameHeaders: []string{"X-Remote-User"},
|
||||
groupHeaders: []string{"X-Remote-Group-1", "X-Remote-Group-2"},
|
||||
extraPrefixHeaders: []string{"X-Remote-Extra-1-", "X-Remote-Extra-2-"},
|
||||
requestHeaders: http.Header{
|
||||
"X-Remote-User": {"Bob"},
|
||||
"X-Remote-Group-1": {"one-a", "one-b"},
|
||||
"X-Remote-Group-2": {"two-a", "two-b"},
|
||||
"X-Remote-extra-1-key1": {"alfa", "bravo"},
|
||||
"X-Remote-Extra-1-Key2": {"charlie", "delta"},
|
||||
"X-Remote-Extra-1-": {"india", "juliet"},
|
||||
"X-Remote-extra-2-": {"kilo", "lima"},
|
||||
"X-Remote-extra-2-Key1": {"echo", "foxtrot"},
|
||||
"X-Remote-Extra-2-key2": {"golf", "hotel"},
|
||||
},
|
||||
expectedUser: &user.DefaultInfo{
|
||||
Name: "Bob",
|
||||
Groups: []string{"one-a", "one-b", "two-a", "two-b"},
|
||||
Extra: map[string][]string{
|
||||
"key1": {"alfa", "bravo", "echo", "foxtrot"},
|
||||
"key2": {"charlie", "delta", "golf", "hotel"},
|
||||
"": {"india", "juliet", "kilo", "lima"},
|
||||
},
|
||||
},
|
||||
expectedOk: true,
|
||||
},
|
||||
}
|
||||
|
||||
for k, testcase := range testcases {
|
||||
auth, err := New(testcase.nameHeaders)
|
||||
auth, err := New(testcase.nameHeaders, testcase.groupHeaders, testcase.extraPrefixHeaders)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user