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:
parent
86c00a2310
commit
c5f348ea2e
109 changed files with 4320 additions and 586 deletions
|
|
@ -32,13 +32,64 @@ import (
|
|||
const (
|
||||
defaultAuthTLSDepth = 1
|
||||
defaultAuthVerifyClient = "on"
|
||||
|
||||
annotationAuthTLSSecret = "auth-tls-secret" //#nosec G101
|
||||
annotationAuthTLSVerifyClient = "auth-tls-verify-client"
|
||||
annotationAuthTLSVerifyDepth = "auth-tls-verify-depth"
|
||||
annotationAuthTLSErrorPage = "auth-tls-error-page"
|
||||
annotationAuthTLSPassCertToUpstream = "auth-tls-pass-certificate-to-upstream" //#nosec G101
|
||||
annotationAuthTLSMatchCN = "auth-tls-match-cn"
|
||||
)
|
||||
|
||||
var (
|
||||
regexChars = regexp.QuoteMeta(`()|=`)
|
||||
authVerifyClientRegex = regexp.MustCompile(`on|off|optional|optional_no_ca`)
|
||||
commonNameRegex = regexp.MustCompile(`CN=`)
|
||||
commonNameRegex = regexp.MustCompile(`^CN=[/\-.\_\~a-zA-Z0-9` + regexChars + `]*$`)
|
||||
redirectRegex = regexp.MustCompile(`^((https?://)?[A-Za-z0-9\-\.]*(:[0-9]+)?/[A-Za-z0-9\-\.]*)?$`)
|
||||
)
|
||||
|
||||
var authTLSAnnotations = parser.Annotation{
|
||||
Group: "authentication",
|
||||
Annotations: parser.AnnotationFields{
|
||||
annotationAuthTLSSecret: {
|
||||
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 secret that contains the certificate chain of allowed certs`,
|
||||
},
|
||||
annotationAuthTLSVerifyClient: {
|
||||
Validator: parser.ValidateRegex(*authVerifyClientRegex, true),
|
||||
Scope: parser.AnnotationScopeLocation,
|
||||
Risk: parser.AnnotationRiskMedium, // Medium as it allows a subset of chars
|
||||
Documentation: `This annotation enables verification of client certificates. Can be "on", "off", "optional" or "optional_no_ca"`,
|
||||
},
|
||||
annotationAuthTLSVerifyDepth: {
|
||||
Validator: parser.ValidateInt,
|
||||
Scope: parser.AnnotationScopeLocation,
|
||||
Risk: parser.AnnotationRiskLow,
|
||||
Documentation: `This annotation defines validation depth between the provided client certificate and the Certification Authority chain.`,
|
||||
},
|
||||
annotationAuthTLSErrorPage: {
|
||||
Validator: parser.ValidateRegex(*redirectRegex, true),
|
||||
Scope: parser.AnnotationScopeLocation,
|
||||
Risk: parser.AnnotationRiskHigh,
|
||||
Documentation: `This annotation defines the URL/Page that user should be redirected in case of a Certificate Authentication Error`,
|
||||
},
|
||||
annotationAuthTLSPassCertToUpstream: {
|
||||
Validator: parser.ValidateBool,
|
||||
Scope: parser.AnnotationScopeLocation,
|
||||
Risk: parser.AnnotationRiskLow,
|
||||
Documentation: `This annotation defines if the received certificates should be passed or not to the upstream server in the header "ssl-client-cert"`,
|
||||
},
|
||||
annotationAuthTLSMatchCN: {
|
||||
Validator: parser.ValidateRegex(*commonNameRegex, true),
|
||||
Scope: parser.AnnotationScopeLocation,
|
||||
Risk: parser.AnnotationRiskHigh,
|
||||
Documentation: `This annotation adds a sanity check for the CN of the client certificate that is sent over using a string / regex starting with "CN="`,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
// Config contains the AuthSSLCert used for mutual authentication
|
||||
// and the configured ValidationDepth
|
||||
type Config struct {
|
||||
|
|
@ -80,11 +131,15 @@ func (assl1 *Config) Equal(assl2 *Config) bool {
|
|||
|
||||
// NewParser creates a new TLS authentication annotation parser
|
||||
func NewParser(resolver resolver.Resolver) parser.IngressAnnotation {
|
||||
return authTLS{resolver}
|
||||
return authTLS{
|
||||
r: resolver,
|
||||
annotationConfig: authTLSAnnotations,
|
||||
}
|
||||
}
|
||||
|
||||
type authTLS struct {
|
||||
r resolver.Resolver
|
||||
r resolver.Resolver
|
||||
annotationConfig parser.Annotation
|
||||
}
|
||||
|
||||
// Parse parses the annotations contained in the ingress
|
||||
|
|
@ -93,15 +148,23 @@ func (a authTLS) Parse(ing *networking.Ingress) (interface{}, error) {
|
|||
var err error
|
||||
config := &Config{}
|
||||
|
||||
tlsauthsecret, err := parser.GetStringAnnotation("auth-tls-secret", ing)
|
||||
tlsauthsecret, err := parser.GetStringAnnotation(annotationAuthTLSSecret, ing, a.annotationConfig.Annotations)
|
||||
if err != nil {
|
||||
return &Config{}, err
|
||||
}
|
||||
|
||||
_, _, err = k8s.ParseNameNS(tlsauthsecret)
|
||||
ns, _, err := k8s.ParseNameNS(tlsauthsecret)
|
||||
if err != nil {
|
||||
return &Config{}, ing_errors.NewLocationDenied(err.Error())
|
||||
}
|
||||
if ns == "" {
|
||||
ns = ing.Namespace
|
||||
}
|
||||
secCfg := a.r.GetSecurityConfiguration()
|
||||
// We don't accept different namespaces for secrets.
|
||||
if !secCfg.AllowCrossNamespaceResources && ns != ing.Namespace {
|
||||
return &Config{}, ing_errors.NewLocationDenied("cross namespace secrets are not supported")
|
||||
}
|
||||
|
||||
authCert, err := a.r.GetAuthCertificate(tlsauthsecret)
|
||||
if err != nil {
|
||||
|
|
@ -110,30 +173,50 @@ func (a authTLS) Parse(ing *networking.Ingress) (interface{}, error) {
|
|||
}
|
||||
config.AuthSSLCert = *authCert
|
||||
|
||||
config.VerifyClient, err = parser.GetStringAnnotation("auth-tls-verify-client", ing)
|
||||
config.VerifyClient, err = parser.GetStringAnnotation(annotationAuthTLSVerifyClient, ing, a.annotationConfig.Annotations)
|
||||
// We can set a default value here in case of validation error
|
||||
if err != nil || !authVerifyClientRegex.MatchString(config.VerifyClient) {
|
||||
config.VerifyClient = defaultAuthVerifyClient
|
||||
}
|
||||
|
||||
config.ValidationDepth, err = parser.GetIntAnnotation("auth-tls-verify-depth", ing)
|
||||
config.ValidationDepth, err = parser.GetIntAnnotation(annotationAuthTLSVerifyDepth, ing, a.annotationConfig.Annotations)
|
||||
// We can set a default value here in case of validation error
|
||||
if err != nil || config.ValidationDepth == 0 {
|
||||
config.ValidationDepth = defaultAuthTLSDepth
|
||||
}
|
||||
|
||||
config.ErrorPage, err = parser.GetStringAnnotation("auth-tls-error-page", ing)
|
||||
config.ErrorPage, err = parser.GetStringAnnotation(annotationAuthTLSErrorPage, ing, a.annotationConfig.Annotations)
|
||||
if err != nil {
|
||||
if ing_errors.IsValidationError(err) {
|
||||
return &Config{}, err
|
||||
}
|
||||
config.ErrorPage = ""
|
||||
}
|
||||
|
||||
config.PassCertToUpstream, err = parser.GetBoolAnnotation("auth-tls-pass-certificate-to-upstream", ing)
|
||||
config.PassCertToUpstream, err = parser.GetBoolAnnotation(annotationAuthTLSPassCertToUpstream, ing, a.annotationConfig.Annotations)
|
||||
if err != nil {
|
||||
if ing_errors.IsValidationError(err) {
|
||||
return &Config{}, err
|
||||
}
|
||||
config.PassCertToUpstream = false
|
||||
}
|
||||
|
||||
config.MatchCN, err = parser.GetStringAnnotation("auth-tls-match-cn", ing)
|
||||
if err != nil || !commonNameRegex.MatchString(config.MatchCN) {
|
||||
config.MatchCN, err = parser.GetStringAnnotation(annotationAuthTLSMatchCN, ing, a.annotationConfig.Annotations)
|
||||
if err != nil {
|
||||
if ing_errors.IsValidationError(err) {
|
||||
return &Config{}, err
|
||||
}
|
||||
config.MatchCN = ""
|
||||
}
|
||||
|
||||
return config, nil
|
||||
}
|
||||
|
||||
func (a authTLS) GetDocumentation() parser.AnnotationFields {
|
||||
return a.annotationConfig.Annotations
|
||||
}
|
||||
|
||||
func (a authTLS) Validate(anns map[string]string) error {
|
||||
maxrisk := parser.StringRiskToRisk(a.r.GetSecurityConfiguration().AnnotationsRiskLevel)
|
||||
return parser.CheckAnnotationRisk(anns, maxrisk, authTLSAnnotations.Annotations)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -93,7 +93,7 @@ func TestAnnotations(t *testing.T) {
|
|||
ing := buildIngress()
|
||||
data := map[string]string{}
|
||||
|
||||
data[parser.GetAnnotationWithPrefix("auth-tls-secret")] = "default/demo-secret"
|
||||
data[parser.GetAnnotationWithPrefix(annotationAuthTLSSecret)] = "default/demo-secret"
|
||||
|
||||
ing.SetAnnotations(data)
|
||||
|
||||
|
|
@ -132,11 +132,11 @@ func TestAnnotations(t *testing.T) {
|
|||
t.Errorf("expected empty string, but got %v", u.MatchCN)
|
||||
}
|
||||
|
||||
data[parser.GetAnnotationWithPrefix("auth-tls-verify-client")] = "off"
|
||||
data[parser.GetAnnotationWithPrefix("auth-tls-verify-depth")] = "2"
|
||||
data[parser.GetAnnotationWithPrefix("auth-tls-error-page")] = "ok.com/error"
|
||||
data[parser.GetAnnotationWithPrefix("auth-tls-pass-certificate-to-upstream")] = "true"
|
||||
data[parser.GetAnnotationWithPrefix("auth-tls-match-cn")] = "CN=hello-app"
|
||||
data[parser.GetAnnotationWithPrefix(annotationAuthTLSVerifyClient)] = "off"
|
||||
data[parser.GetAnnotationWithPrefix(annotationAuthTLSVerifyDepth)] = "2"
|
||||
data[parser.GetAnnotationWithPrefix(annotationAuthTLSErrorPage)] = "ok.com/error"
|
||||
data[parser.GetAnnotationWithPrefix(annotationAuthTLSPassCertToUpstream)] = "true"
|
||||
data[parser.GetAnnotationWithPrefix(annotationAuthTLSMatchCN)] = "CN=(hello-app|ok|goodbye)"
|
||||
|
||||
ing.SetAnnotations(data)
|
||||
|
||||
|
|
@ -165,8 +165,8 @@ func TestAnnotations(t *testing.T) {
|
|||
if u.PassCertToUpstream != true {
|
||||
t.Errorf("expected %v but got %v", true, u.PassCertToUpstream)
|
||||
}
|
||||
if u.MatchCN != "CN=hello-app" {
|
||||
t.Errorf("expected %v but got %v", "CN=hello-app", u.MatchCN)
|
||||
if u.MatchCN != "CN=(hello-app|ok|goodbye)" {
|
||||
t.Errorf("expected %v but got %v", "CN=(hello-app|ok|goodbye)", u.MatchCN)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -182,15 +182,24 @@ func TestInvalidAnnotations(t *testing.T) {
|
|||
}
|
||||
|
||||
// Invalid NameSpace
|
||||
data[parser.GetAnnotationWithPrefix("auth-tls-secret")] = "demo-secret"
|
||||
data[parser.GetAnnotationWithPrefix(annotationAuthTLSSecret)] = "demo-secret"
|
||||
ing.SetAnnotations(data)
|
||||
_, err = NewParser(fakeSecret).Parse(ing)
|
||||
if err == nil {
|
||||
t.Errorf("Expected error with ingress but got nil")
|
||||
}
|
||||
|
||||
// Invalid Cross NameSpace
|
||||
data[parser.GetAnnotationWithPrefix(annotationAuthTLSSecret)] = "nondefault/demo-secret"
|
||||
ing.SetAnnotations(data)
|
||||
_, err = NewParser(fakeSecret).Parse(ing)
|
||||
expErr := errors.NewLocationDenied("cross namespace secrets are not supported")
|
||||
if err.Error() != expErr.Error() {
|
||||
t.Errorf("received error is different from cross namespace error: %s Expected %s", err, expErr)
|
||||
}
|
||||
|
||||
// Invalid Auth Certificate
|
||||
data[parser.GetAnnotationWithPrefix("auth-tls-secret")] = "default/invalid-demo-secret"
|
||||
data[parser.GetAnnotationWithPrefix(annotationAuthTLSSecret)] = "default/invalid-demo-secret"
|
||||
ing.SetAnnotations(data)
|
||||
_, err = NewParser(fakeSecret).Parse(ing)
|
||||
if err == nil {
|
||||
|
|
@ -198,11 +207,38 @@ func TestInvalidAnnotations(t *testing.T) {
|
|||
}
|
||||
|
||||
// Invalid optional Annotations
|
||||
data[parser.GetAnnotationWithPrefix("auth-tls-secret")] = "default/demo-secret"
|
||||
data[parser.GetAnnotationWithPrefix("auth-tls-verify-client")] = "w00t"
|
||||
data[parser.GetAnnotationWithPrefix("auth-tls-verify-depth")] = "abcd"
|
||||
data[parser.GetAnnotationWithPrefix("auth-tls-pass-certificate-to-upstream")] = "nahh"
|
||||
data[parser.GetAnnotationWithPrefix("auth-tls-match-cn")] = "<script>nope</script>"
|
||||
data[parser.GetAnnotationWithPrefix(annotationAuthTLSSecret)] = "default/demo-secret"
|
||||
|
||||
data[parser.GetAnnotationWithPrefix(annotationAuthTLSVerifyClient)] = "w00t"
|
||||
ing.SetAnnotations(data)
|
||||
_, err = NewParser(fakeSecret).Parse(ing)
|
||||
if err != nil {
|
||||
t.Errorf("Error should be nil and verify client should be defaulted")
|
||||
}
|
||||
|
||||
data[parser.GetAnnotationWithPrefix(annotationAuthTLSVerifyDepth)] = "abcd"
|
||||
ing.SetAnnotations(data)
|
||||
_, err = NewParser(fakeSecret).Parse(ing)
|
||||
if err != nil {
|
||||
t.Errorf("Error should be nil and verify depth should be defaulted")
|
||||
}
|
||||
|
||||
data[parser.GetAnnotationWithPrefix(annotationAuthTLSPassCertToUpstream)] = "nahh"
|
||||
ing.SetAnnotations(data)
|
||||
_, err = NewParser(fakeSecret).Parse(ing)
|
||||
if err == nil {
|
||||
t.Errorf("Expected error with ingress but got nil")
|
||||
}
|
||||
delete(data, parser.GetAnnotationWithPrefix(annotationAuthTLSPassCertToUpstream))
|
||||
|
||||
data[parser.GetAnnotationWithPrefix(annotationAuthTLSMatchCN)] = "<script>nope</script>"
|
||||
ing.SetAnnotations(data)
|
||||
_, err = NewParser(fakeSecret).Parse(ing)
|
||||
if err == nil {
|
||||
t.Errorf("Expected error with ingress CN but got nil")
|
||||
}
|
||||
delete(data, parser.GetAnnotationWithPrefix(annotationAuthTLSMatchCN))
|
||||
|
||||
ing.SetAnnotations(data)
|
||||
|
||||
i, err := NewParser(fakeSecret).Parse(ing)
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue