mirror of
https://github.com/k3s-io/kubernetes.git
synced 2025-07-22 19:31:44 +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
|
report-prefix
|
||||||
requestheader-allowed-names
|
requestheader-allowed-names
|
||||||
requestheader-client-ca-file
|
requestheader-client-ca-file
|
||||||
|
requestheader-extra-headers-prefix
|
||||||
|
requestheader-group-headers
|
||||||
requestheader-username-headers
|
requestheader-username-headers
|
||||||
require-kubeconfig
|
require-kubeconfig
|
||||||
required-contexts
|
required-contexts
|
||||||
|
@ -43,6 +43,11 @@ import (
|
|||||||
type RequestHeaderConfig struct {
|
type RequestHeaderConfig struct {
|
||||||
// UsernameHeaders are the headers to check (in order, case-insensitively) for an identity. The first header with a value wins.
|
// UsernameHeaders are the headers to check (in order, case-insensitively) for an identity. The first header with a value wins.
|
||||||
UsernameHeaders []string
|
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 points to CA bundle file which is used verify the identity of the front proxy
|
||||||
ClientCA string
|
ClientCA string
|
||||||
// AllowedClientNames is a list of common names that may be presented by the authenticating front proxy. Empty means: accept any.
|
// 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.ClientCA,
|
||||||
config.RequestHeaderConfig.AllowedClientNames,
|
config.RequestHeaderConfig.AllowedClientNames,
|
||||||
config.RequestHeaderConfig.UsernameHeaders,
|
config.RequestHeaderConfig.UsernameHeaders,
|
||||||
|
config.RequestHeaderConfig.GroupHeaders,
|
||||||
|
config.RequestHeaderConfig.ExtraHeaderPrefixes,
|
||||||
)
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, nil, err
|
return nil, nil, err
|
||||||
|
@ -63,12 +63,6 @@ type PasswordFileAuthenticationOptions struct {
|
|||||||
BasicAuthFile string
|
BasicAuthFile string
|
||||||
}
|
}
|
||||||
|
|
||||||
type RequestHeaderAuthenticationOptions struct {
|
|
||||||
UsernameHeaders []string
|
|
||||||
ClientCAFile string
|
|
||||||
AllowedNames []string
|
|
||||||
}
|
|
||||||
|
|
||||||
type ServiceAccountAuthenticationOptions struct {
|
type ServiceAccountAuthenticationOptions struct {
|
||||||
KeyFiles []string
|
KeyFiles []string
|
||||||
Lookup bool
|
Lookup bool
|
||||||
@ -206,17 +200,7 @@ func (s *BuiltInAuthenticationOptions) AddFlags(fs *pflag.FlagSet) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if s.RequestHeader != nil {
|
if s.RequestHeader != nil {
|
||||||
fs.StringSliceVar(&s.RequestHeader.UsernameHeaders, "requestheader-username-headers", s.RequestHeader.UsernameHeaders, ""+
|
s.RequestHeader.AddFlags(fs)
|
||||||
"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.")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if s.ServiceAccounts != nil {
|
if s.ServiceAccounts != nil {
|
||||||
@ -275,7 +259,7 @@ func (s *BuiltInAuthenticationOptions) ToAuthenticationConfig(clientCAFile strin
|
|||||||
}
|
}
|
||||||
|
|
||||||
if s.RequestHeader != nil {
|
if s.RequestHeader != nil {
|
||||||
ret.RequestHeaderConfig = s.RequestHeader.AuthenticationRequestHeaderConfig()
|
ret.RequestHeaderConfig = s.RequestHeader.ToAuthenticationRequestHeaderConfig()
|
||||||
}
|
}
|
||||||
|
|
||||||
if s.ServiceAccounts != nil {
|
if s.ServiceAccounts != nil {
|
||||||
@ -295,15 +279,45 @@ func (s *BuiltInAuthenticationOptions) ToAuthenticationConfig(clientCAFile strin
|
|||||||
return ret
|
return ret
|
||||||
}
|
}
|
||||||
|
|
||||||
// AuthenticationRequestHeaderConfig returns an authenticator config object for these options
|
type RequestHeaderAuthenticationOptions struct {
|
||||||
// if necessary. nil otherwise.
|
UsernameHeaders []string
|
||||||
func (s *RequestHeaderAuthenticationOptions) AuthenticationRequestHeaderConfig() *authenticator.RequestHeaderConfig {
|
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 {
|
if len(s.UsernameHeaders) == 0 {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
return &authenticator.RequestHeaderConfig{
|
return &authenticator.RequestHeaderConfig{
|
||||||
UsernameHeaders: s.UsernameHeaders,
|
UsernameHeaders: s.UsernameHeaders,
|
||||||
|
GroupHeaders: s.GroupHeaders,
|
||||||
|
ExtraHeaderPrefixes: s.ExtraHeaderPrefixes,
|
||||||
ClientCA: s.ClientCAFile,
|
ClientCA: s.ClientCAFile,
|
||||||
AllowedClientNames: s.AllowedNames,
|
AllowedClientNames: s.AllowedNames,
|
||||||
}
|
}
|
||||||
|
@ -33,23 +33,51 @@ import (
|
|||||||
type requestHeaderAuthRequestHandler struct {
|
type requestHeaderAuthRequestHandler struct {
|
||||||
// nameHeaders are the headers to check (in order, case-insensitively) for an identity. The first header with a value wins.
|
// nameHeaders are the headers to check (in order, case-insensitively) for an identity. The first header with a value wins.
|
||||||
nameHeaders []string
|
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) {
|
func New(nameHeaders []string, groupHeaders []string, extraHeaderPrefixes []string) (authenticator.Request, error) {
|
||||||
headers := []string{}
|
trimmedNameHeaders, err := trimHeaders(nameHeaders...)
|
||||||
for _, headerName := range 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)
|
trimmedHeader := strings.TrimSpace(headerName)
|
||||||
if len(trimmedHeader) == 0 {
|
if len(trimmedHeader) == 0 {
|
||||||
return nil, fmt.Errorf("empty header %q", headerName)
|
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) {
|
func NewSecure(clientCA string, proxyClientNames []string, nameHeaders []string, groupHeaders []string, extraHeaderPrefixes []string) (authenticator.Request, error) {
|
||||||
headerAuthenticator, err := New(nameHeaders)
|
headerAuthenticator, err := New(nameHeaders, groupHeaders, extraHeaderPrefixes)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@ -81,8 +109,27 @@ func (a *requestHeaderAuthRequestHandler) AuthenticateRequest(req *http.Request)
|
|||||||
if len(name) == 0 {
|
if len(name) == 0 {
|
||||||
return nil, false, nil
|
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 {
|
func headerValue(h http.Header, headerNames []string) string {
|
||||||
@ -94,3 +141,38 @@ func headerValue(h http.Header, headerNames []string) string {
|
|||||||
}
|
}
|
||||||
return ""
|
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) {
|
func TestRequestHeader(t *testing.T) {
|
||||||
testcases := map[string]struct {
|
testcases := map[string]struct {
|
||||||
nameHeaders []string
|
nameHeaders []string
|
||||||
|
groupHeaders []string
|
||||||
|
extraPrefixHeaders []string
|
||||||
requestHeaders http.Header
|
requestHeaders http.Header
|
||||||
|
|
||||||
expectedUser user.Info
|
expectedUser user.Info
|
||||||
expectedOk bool
|
expectedOk bool
|
||||||
}{
|
}{
|
||||||
"empty": {},
|
"empty": {},
|
||||||
"no match": {
|
"user no match": {
|
||||||
nameHeaders: []string{"X-Remote-User"},
|
nameHeaders: []string{"X-Remote-User"},
|
||||||
},
|
},
|
||||||
"match": {
|
"user match": {
|
||||||
nameHeaders: []string{"X-Remote-User"},
|
nameHeaders: []string{"X-Remote-User"},
|
||||||
requestHeaders: http.Header{"X-Remote-User": {"Bob"}},
|
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,
|
expectedOk: true,
|
||||||
},
|
},
|
||||||
"exact match": {
|
"user exact match": {
|
||||||
nameHeaders: []string{"X-Remote-User"},
|
nameHeaders: []string{"X-Remote-User"},
|
||||||
requestHeaders: http.Header{
|
requestHeaders: http.Header{
|
||||||
"Prefixed-X-Remote-User-With-Suffix": {"Bob"},
|
"Prefixed-X-Remote-User-With-Suffix": {"Bob"},
|
||||||
"X-Remote-User-With-Suffix": {"Bob"},
|
"X-Remote-User-With-Suffix": {"Bob"},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
"first match": {
|
"user first match": {
|
||||||
nameHeaders: []string{
|
nameHeaders: []string{
|
||||||
"X-Remote-User",
|
"X-Remote-User",
|
||||||
"A-Second-X-Remote-User",
|
"A-Second-X-Remote-User",
|
||||||
@ -59,19 +65,83 @@ func TestRequestHeader(t *testing.T) {
|
|||||||
"X-Remote-User": {"", "First header, second value"},
|
"X-Remote-User": {"", "First header, second value"},
|
||||||
"A-Second-X-Remote-User": {"Second header, first value", "Second header, second value"},
|
"A-Second-X-Remote-User": {"Second header, first value", "Second header, second value"},
|
||||||
"Another-X-Remote-User": {"Third header, first 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,
|
expectedOk: true,
|
||||||
},
|
},
|
||||||
"case-insensitive": {
|
"user case-insensitive": {
|
||||||
nameHeaders: []string{"x-REMOTE-user"}, // configured headers can be 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
|
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,
|
expectedOk: true,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
for k, testcase := range testcases {
|
for k, testcase := range testcases {
|
||||||
auth, err := New(testcase.nameHeaders)
|
auth, err := New(testcase.nameHeaders, testcase.groupHeaders, testcase.extraPrefixHeaders)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user