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

@ -32,13 +32,56 @@ import (
"k8s.io/ingress-nginx/pkg/util/file"
)
const (
authSecretTypeAnnotation = "auth-secret-type" //#nosec G101
authRealmAnnotation = "auth-realm"
authTypeAnnotation = "auth-type"
// This should be exported as it is imported by other packages
AuthSecretAnnotation = "auth-secret" //#nosec G101
)
var (
authTypeRegex = regexp.MustCompile(`basic|digest`)
authTypeRegex = regexp.MustCompile(`basic|digest`)
authSecretTypeRegex = regexp.MustCompile(`auth-file|auth-map`)
// AuthDirectory default directory used to store files
// to authenticate request
AuthDirectory = "/etc/ingress-controller/auth"
)
var AuthSecretConfig = parser.AnnotationConfig{
Validator: parser.ValidateRegex(*parser.BasicCharsRegex, true),
Scope: parser.AnnotationScopeLocation,
Risk: parser.AnnotationRiskMedium, // Medium as it allows a subset of chars
Documentation: `This annotation defines the name of the Secret that contains the usernames and passwords which are granted access to the paths defined in the Ingress rules. `,
}
var authSecretAnnotations = parser.Annotation{
Group: "authentication",
Annotations: parser.AnnotationFields{
AuthSecretAnnotation: AuthSecretConfig,
authSecretTypeAnnotation: {
Validator: parser.ValidateRegex(*authSecretTypeRegex, true),
Scope: parser.AnnotationScopeLocation,
Risk: parser.AnnotationRiskLow,
Documentation: `This annotation what is the format of auth-secret value. Can be "auth-file" that defines the content of an htpasswd file, or "auth-map" where each key
is a user and each value is the password.`,
},
authRealmAnnotation: {
Validator: parser.ValidateRegex(*parser.CharsWithSpace, false),
Scope: parser.AnnotationScopeLocation,
Risk: parser.AnnotationRiskMedium, // Medium as it allows a subset of chars
Documentation: `This annotation defines the realm (message) that should be shown to user when authentication is requested.`,
},
authTypeAnnotation: {
Validator: parser.ValidateRegex(*authTypeRegex, true),
Scope: parser.AnnotationScopeLocation,
Risk: parser.AnnotationRiskLow,
Documentation: `This annotation defines the basic authentication type. Should be "basic" or "digest"`,
},
},
}
const (
fileAuth = "auth-file"
mapAuth = "auth-map"
@ -85,13 +128,18 @@ func (bd1 *Config) Equal(bd2 *Config) bool {
}
type auth struct {
r resolver.Resolver
authDirectory string
r resolver.Resolver
authDirectory string
annotationConfig parser.Annotation
}
// NewParser creates a new authentication annotation parser
func NewParser(authDirectory string, r resolver.Resolver) parser.IngressAnnotation {
return auth{r, authDirectory}
return auth{
r: r,
authDirectory: authDirectory,
annotationConfig: authSecretAnnotations,
}
}
// Parse parses the annotations contained in the ingress
@ -99,7 +147,7 @@ func NewParser(authDirectory string, r resolver.Resolver) parser.IngressAnnotati
// and generated an htpasswd compatible file to be used as source
// during the authentication process
func (a auth) Parse(ing *networking.Ingress) (interface{}, error) {
at, err := parser.GetStringAnnotation("auth-type", ing)
at, err := parser.GetStringAnnotation(authTypeAnnotation, ing, a.annotationConfig.Annotations)
if err != nil {
return nil, err
}
@ -109,12 +157,15 @@ func (a auth) Parse(ing *networking.Ingress) (interface{}, error) {
}
var secretType string
secretType, err = parser.GetStringAnnotation("auth-secret-type", ing)
secretType, err = parser.GetStringAnnotation(authSecretTypeAnnotation, ing, a.annotationConfig.Annotations)
if err != nil {
if ing_errors.IsValidationError(err) {
return nil, err
}
secretType = fileAuth
}
s, err := parser.GetStringAnnotation("auth-secret", ing)
s, err := parser.GetStringAnnotation(AuthSecretAnnotation, ing, a.annotationConfig.Annotations)
if err != nil {
return nil, ing_errors.LocationDenied{
Reason: fmt.Errorf("error reading secret name from annotation: %w", err),
@ -131,6 +182,13 @@ func (a auth) Parse(ing *networking.Ingress) (interface{}, error) {
if sns == "" {
sns = ing.Namespace
}
secCfg := a.r.GetSecurityConfiguration()
// We don't accept different namespaces for secrets.
if !secCfg.AllowCrossNamespaceResources && sns != ing.Namespace {
return nil, ing_errors.LocationDenied{
Reason: fmt.Errorf("cross namespace usage of secrets is not allowed"),
}
}
name := fmt.Sprintf("%v/%v", sns, sname)
secret, err := a.r.GetSecret(name)
@ -140,7 +198,10 @@ func (a auth) Parse(ing *networking.Ingress) (interface{}, error) {
}
}
realm, _ := parser.GetStringAnnotation("auth-realm", ing)
realm, err := parser.GetStringAnnotation(authRealmAnnotation, ing, a.annotationConfig.Annotations)
if ing_errors.IsValidationError(err) {
return nil, err
}
passFilename := fmt.Sprintf("%v/%v-%v-%v.passwd", a.authDirectory, ing.GetNamespace(), ing.UID, secret.UID)
@ -210,3 +271,12 @@ func dumpSecretAuthMap(filename string, secret *api.Secret) error {
return nil
}
func (a auth) GetDocumentation() parser.AnnotationFields {
return a.annotationConfig.Annotations
}
func (a auth) Validate(anns map[string]string) error {
maxrisk := parser.StringRiskToRisk(a.r.GetSecurityConfiguration().AnnotationsRiskLevel)
return parser.CheckAnnotationRisk(anns, maxrisk, authSecretAnnotations.Annotations)
}