Implement annotation validation (#9673)

* Add validation to all annotations

* Add annotation validation for fcgi

* Fix reviews and fcgi e2e

* Add flag to disable cross namespace validation

* Add risk, flag for validation, tests

* Add missing formating

* Enable validation by default on tests

* Test validation flag

* remove ajp from list

* Finalize validation changes

* Add validations to CI

* Update helm docs

* Fix code review

* Use a better name for annotation risk
This commit is contained in:
Ricardo Katz 2023-07-22 00:32:07 -03:00 committed by GitHub
parent 86c00a2310
commit c5f348ea2e
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
109 changed files with 4320 additions and 586 deletions

View file

@ -24,6 +24,7 @@ import (
"k8s.io/klog/v2"
networking "k8s.io/api/networking/v1"
"k8s.io/client-go/tools/cache"
"k8s.io/ingress-nginx/internal/ingress/annotations/parser"
ing_errors "k8s.io/ingress-nginx/internal/ingress/errors"
@ -31,6 +32,118 @@ import (
"k8s.io/ingress-nginx/pkg/util/sets"
)
const (
authReqURLAnnotation = "auth-url"
authReqMethodAnnotation = "auth-method"
authReqSigninAnnotation = "auth-signin"
authReqSigninRedirParamAnnotation = "auth-signin-redirect-param"
authReqSnippetAnnotation = "auth-snippet"
authReqCacheKeyAnnotation = "auth-cache-key"
authReqKeepaliveAnnotation = "auth-keepalive"
authReqKeepaliveRequestsAnnotation = "auth-keepalive-requests"
authReqKeepaliveTimeout = "auth-keepalive-timeout"
authReqCacheDuration = "auth-cache-duration"
authReqResponseHeadersAnnotation = "auth-response-headers"
authReqProxySetHeadersAnnotation = "auth-proxy-set-headers"
authReqRequestRedirectAnnotation = "auth-request-redirect"
authReqAlwaysSetCookieAnnotation = "auth-always-set-cookie"
// This should be exported as it is imported by other packages
AuthSecretAnnotation = "auth-secret"
)
var authReqAnnotations = parser.Annotation{
Group: "authentication",
Annotations: parser.AnnotationFields{
authReqURLAnnotation: {
Validator: parser.ValidateRegex(*parser.URLWithNginxVariableRegex, true),
Scope: parser.AnnotationScopeLocation,
Risk: parser.AnnotationRiskHigh,
Documentation: `This annotation allows to indicate the URL where the HTTP request should be sent`,
},
authReqMethodAnnotation: {
Validator: parser.ValidateRegex(*methodsRegex, true),
Scope: parser.AnnotationScopeLocation,
Risk: parser.AnnotationRiskLow,
Documentation: `This annotation allows to specify the HTTP method to use`,
},
authReqSigninAnnotation: {
Validator: parser.ValidateRegex(*parser.URLWithNginxVariableRegex, true),
Scope: parser.AnnotationScopeLocation,
Risk: parser.AnnotationRiskHigh,
Documentation: `This annotation allows to specify the location of the error page`,
},
authReqSigninRedirParamAnnotation: {
Validator: parser.ValidateRegex(*parser.URLIsValidRegex, true),
Scope: parser.AnnotationScopeLocation,
Risk: parser.AnnotationRiskMedium,
Documentation: `This annotation allows to specify the URL parameter in the error page which should contain the original URL for a failed signin request`,
},
authReqSnippetAnnotation: {
Validator: parser.ValidateNull,
Scope: parser.AnnotationScopeLocation,
Risk: parser.AnnotationRiskCritical,
Documentation: `This annotation allows to specify a custom snippet to use with external authentication`,
},
authReqCacheKeyAnnotation: {
Validator: parser.ValidateRegex(*parser.NGINXVariable, true),
Scope: parser.AnnotationScopeLocation,
Risk: parser.AnnotationRiskMedium,
Documentation: `This annotation enables caching for auth requests.`,
},
authReqKeepaliveAnnotation: {
Validator: parser.ValidateInt,
Scope: parser.AnnotationScopeLocation,
Risk: parser.AnnotationRiskLow,
Documentation: `This annotation specifies the maximum number of keepalive connections to auth-url. Only takes effect when no variables are used in the host part of the URL`,
},
authReqKeepaliveRequestsAnnotation: {
Validator: parser.ValidateInt,
Scope: parser.AnnotationScopeLocation,
Risk: parser.AnnotationRiskLow,
Documentation: `This annotation defines the maximum number of requests that can be served through one keepalive connection`,
},
authReqKeepaliveTimeout: {
Validator: parser.ValidateInt,
Scope: parser.AnnotationScopeLocation,
Risk: parser.AnnotationRiskLow,
Documentation: `This annotation specifies a duration in seconds which an idle keepalive connection to an upstream server will stay open`,
},
authReqCacheDuration: {
Validator: parser.ValidateRegex(*parser.ExtendedCharsRegex, false),
Scope: parser.AnnotationScopeLocation,
Risk: parser.AnnotationRiskMedium,
Documentation: `This annotation allows to specify a caching time for auth responses based on their response codes, e.g. 200 202 30m`,
},
authReqResponseHeadersAnnotation: {
Validator: parser.ValidateRegex(*parser.HeadersVariable, true),
Scope: parser.AnnotationScopeLocation,
Risk: parser.AnnotationRiskMedium,
Documentation: `This annotation sets the headers to pass to backend once authentication request completes. They should be separated by comma.`,
},
authReqProxySetHeadersAnnotation: {
Validator: parser.ValidateRegex(*parser.BasicCharsRegex, true),
Scope: parser.AnnotationScopeLocation,
Risk: parser.AnnotationRiskMedium,
Documentation: `This annotation sets the name of a ConfigMap that specifies headers to pass to the authentication service.
Only ConfigMaps on the same namespace are allowed`,
},
authReqRequestRedirectAnnotation: {
Validator: parser.ValidateRegex(*parser.URLIsValidRegex, true),
Scope: parser.AnnotationScopeLocation,
Risk: parser.AnnotationRiskMedium,
Documentation: `This annotation allows to specify the X-Auth-Request-Redirect header value`,
},
authReqAlwaysSetCookieAnnotation: {
Validator: parser.ValidateBool,
Scope: parser.AnnotationScopeLocation,
Risk: parser.AnnotationRiskLow,
Documentation: `This annotation enables setting a cookie returned by auth request.
By default, the cookie will be set only if an upstream reports with the code 200, 201, 204, 206, 301, 302, 303, 304, 307, or 308`,
},
},
}
// Config returns external authentication configuration for an Ingress rule
type Config struct {
URL string `json:"url"`
@ -121,7 +234,7 @@ func (e1 *Config) Equal(e2 *Config) bool {
}
var (
methods = []string{"GET", "HEAD", "POST", "PUT", "PATCH", "DELETE", "CONNECT", "OPTIONS", "TRACE"}
methodsRegex = regexp.MustCompile("(GET|HEAD|POST|PUT|PATCH|DELETE|CONNECT|OPTIONS|TRACE)")
headerRegexp = regexp.MustCompile(`^[a-zA-Z\d\-_]+$`)
statusCodeRegex = regexp.MustCompile(`^[\d]{3}$`)
durationRegex = regexp.MustCompile(`^[\d]+(ms|s|m|h|d|w|M|y)$`) // see http://nginx.org/en/docs/syntax.html
@ -129,16 +242,7 @@ var (
// ValidMethod checks is the provided string a valid HTTP method
func ValidMethod(method string) bool {
if len(method) == 0 {
return false
}
for _, m := range methods {
if method == m {
return true
}
}
return false
return methodsRegex.MatchString(method)
}
// ValidHeader checks is the provided string satisfies the header's name regex
@ -173,19 +277,23 @@ func ValidCacheDuration(duration string) bool {
}
type authReq struct {
r resolver.Resolver
r resolver.Resolver
annotationConfig parser.Annotation
}
// NewParser creates a new authentication request annotation parser
func NewParser(r resolver.Resolver) parser.IngressAnnotation {
return authReq{r}
return authReq{
r: r,
annotationConfig: authReqAnnotations,
}
}
// ParseAnnotations parses the annotations contained in the ingress
// rule used to use an Config URL as source for authentication
func (a authReq) Parse(ing *networking.Ingress) (interface{}, error) {
// Required Parameters
urlString, err := parser.GetStringAnnotation("auth-url", ing)
urlString, err := parser.GetStringAnnotation(authReqURLAnnotation, ing, a.annotationConfig.Annotations)
if err != nil {
return nil, err
}
@ -195,33 +303,44 @@ func (a authReq) Parse(ing *networking.Ingress) (interface{}, error) {
return nil, ing_errors.LocationDenied{Reason: fmt.Errorf("could not parse auth-url annotation: %v", err)}
}
authMethod, _ := parser.GetStringAnnotation("auth-method", ing)
if len(authMethod) != 0 && !ValidMethod(authMethod) {
return nil, ing_errors.NewLocationDenied("invalid HTTP method")
authMethod, err := parser.GetStringAnnotation(authReqMethodAnnotation, ing, a.annotationConfig.Annotations)
if err != nil {
if ing_errors.IsValidationError(err) {
return nil, ing_errors.NewLocationDenied("invalid HTTP method")
}
}
// Optional Parameters
signIn, err := parser.GetStringAnnotation("auth-signin", ing)
signIn, err := parser.GetStringAnnotation(authReqSigninAnnotation, ing, a.annotationConfig.Annotations)
if err != nil {
if ing_errors.IsValidationError(err) {
klog.Warningf("%s value is invalid: %s", authReqSigninAnnotation, err)
}
klog.V(3).InfoS("auth-signin annotation is undefined and will not be set")
}
signInRedirectParam, err := parser.GetStringAnnotation("auth-signin-redirect-param", ing)
signInRedirectParam, err := parser.GetStringAnnotation(authReqSigninRedirParamAnnotation, ing, a.annotationConfig.Annotations)
if err != nil {
if ing_errors.IsValidationError(err) {
klog.Warningf("%s value is invalid: %s", authReqSigninRedirParamAnnotation, err)
}
klog.V(3).Infof("auth-signin-redirect-param annotation is undefined and will not be set")
}
authSnippet, err := parser.GetStringAnnotation("auth-snippet", ing)
authSnippet, err := parser.GetStringAnnotation(authReqSnippetAnnotation, ing, a.annotationConfig.Annotations)
if err != nil {
klog.V(3).InfoS("auth-snippet annotation is undefined and will not be set")
}
authCacheKey, err := parser.GetStringAnnotation("auth-cache-key", ing)
authCacheKey, err := parser.GetStringAnnotation(authReqCacheKeyAnnotation, ing, a.annotationConfig.Annotations)
if err != nil {
if ing_errors.IsValidationError(err) {
klog.Warningf("%s value is invalid: %s", authReqCacheKeyAnnotation, err)
}
klog.V(3).InfoS("auth-cache-key annotation is undefined and will not be set")
}
keepaliveConnections, err := parser.GetIntAnnotation("auth-keepalive", ing)
keepaliveConnections, err := parser.GetIntAnnotation(authReqKeepaliveAnnotation, ing, a.annotationConfig.Annotations)
if err != nil {
klog.V(3).InfoS("auth-keepalive annotation is undefined and will be set to its default value")
keepaliveConnections = defaultKeepaliveConnections
@ -238,9 +357,9 @@ func (a authReq) Parse(ing *networking.Ingress) (interface{}, error) {
}
}
keepaliveRequests, err := parser.GetIntAnnotation("auth-keepalive-requests", ing)
keepaliveRequests, err := parser.GetIntAnnotation(authReqKeepaliveRequestsAnnotation, ing, a.annotationConfig.Annotations)
if err != nil {
klog.V(3).InfoS("auth-keepalive-requests annotation is undefined and will be set to its default value")
klog.V(3).InfoS("auth-keepalive-requests annotation is undefined or invalid and will be set to its default value")
keepaliveRequests = defaultKeepaliveRequests
}
if keepaliveRequests <= 0 {
@ -248,7 +367,7 @@ func (a authReq) Parse(ing *networking.Ingress) (interface{}, error) {
keepaliveConnections = 0
}
keepaliveTimeout, err := parser.GetIntAnnotation("auth-keepalive-timeout", ing)
keepaliveTimeout, err := parser.GetIntAnnotation(authReqKeepaliveTimeout, ing, a.annotationConfig.Annotations)
if err != nil {
klog.V(3).InfoS("auth-keepalive-timeout annotation is undefined and will be set to its default value")
keepaliveTimeout = defaultKeepaliveTimeout
@ -258,14 +377,20 @@ func (a authReq) Parse(ing *networking.Ingress) (interface{}, error) {
keepaliveConnections = 0
}
durstr, _ := parser.GetStringAnnotation("auth-cache-duration", ing)
durstr, err := parser.GetStringAnnotation(authReqCacheDuration, ing, a.annotationConfig.Annotations)
if err != nil && ing_errors.IsValidationError(err) {
return nil, fmt.Errorf("%s contains invalid value", authReqCacheDuration)
}
authCacheDuration, err := ParseStringToCacheDurations(durstr)
if err != nil {
return nil, err
}
responseHeaders := []string{}
hstr, _ := parser.GetStringAnnotation("auth-response-headers", ing)
hstr, err := parser.GetStringAnnotation(authReqResponseHeadersAnnotation, ing, a.annotationConfig.Annotations)
if err != nil && ing_errors.IsValidationError(err) {
return nil, ing_errors.NewLocationDenied("validation error")
}
if len(hstr) != 0 {
harr := strings.Split(hstr, ",")
for _, header := range harr {
@ -279,9 +404,28 @@ func (a authReq) Parse(ing *networking.Ingress) (interface{}, error) {
}
}
proxySetHeaderMap, err := parser.GetStringAnnotation("auth-proxy-set-headers", ing)
proxySetHeaderMap, err := parser.GetStringAnnotation(authReqProxySetHeadersAnnotation, ing, a.annotationConfig.Annotations)
if err != nil {
klog.V(3).InfoS("auth-set-proxy-headers annotation is undefined and will not be set")
klog.V(3).InfoS("auth-set-proxy-headers annotation is undefined and will not be set", "err", err)
}
cns, _, err := cache.SplitMetaNamespaceKey(proxySetHeaderMap)
if err != nil {
return nil, ing_errors.LocationDenied{
Reason: fmt.Errorf("error reading configmap name %s from annotation: %w", proxySetHeaderMap, err),
}
}
if cns == "" {
cns = ing.Namespace
}
secCfg := a.r.GetSecurityConfiguration()
// We don't accept different namespaces for secrets.
if !secCfg.AllowCrossNamespaceResources && cns != ing.Namespace {
return nil, ing_errors.LocationDenied{
Reason: fmt.Errorf("cross namespace usage of secrets is not allowed"),
}
}
var proxySetHeaders map[string]string
@ -301,9 +445,15 @@ func (a authReq) Parse(ing *networking.Ingress) (interface{}, error) {
proxySetHeaders = proxySetHeadersMapContents.Data
}
requestRedirect, _ := parser.GetStringAnnotation("auth-request-redirect", ing)
requestRedirect, err := parser.GetStringAnnotation(authReqRequestRedirectAnnotation, ing, a.annotationConfig.Annotations)
if err != nil && ing_errors.IsValidationError(err) {
return nil, fmt.Errorf("%s is invalid: %w", authReqRequestRedirectAnnotation, err)
}
alwaysSetCookie, _ := parser.GetBoolAnnotation("auth-always-set-cookie", ing)
alwaysSetCookie, err := parser.GetBoolAnnotation(authReqAlwaysSetCookieAnnotation, ing, a.annotationConfig.Annotations)
if err != nil && ing_errors.IsValidationError(err) {
return nil, fmt.Errorf("%s is invalid: %w", authReqAlwaysSetCookieAnnotation, err)
}
return &Config{
URL: urlString,
@ -348,3 +498,12 @@ func ParseStringToCacheDurations(input string) ([]string, error) {
}
return authCacheDuration, nil
}
func (a authReq) GetDocumentation() parser.AnnotationFields {
return a.annotationConfig.Annotations
}
func (a authReq) Validate(anns map[string]string) error {
maxrisk := parser.StringRiskToRisk(a.r.GetSecurityConfiguration().AnnotationsRiskLevel)
return parser.CheckAnnotationRisk(anns, maxrisk, authReqAnnotations.Annotations)
}

View file

@ -192,11 +192,13 @@ func TestHeaderAnnotations(t *testing.T) {
i, err := NewParser(&resolver.Mock{}).Parse(ing)
if test.expErr {
if err == nil {
t.Error("expected error but retuned nil")
t.Errorf("%v expected error but retuned nil", test.title)
}
continue
}
if err != nil {
t.Errorf("no error was expected but %v happened in %s", err, test.title)
}
u, ok := i.(*Config)
if !ok {
t.Errorf("%v: expected an External type", test.title)