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

@ -319,6 +319,55 @@ jobs:
name: e2e-test-reports-${{ matrix.k8s }}
path: 'test/junitreports/report*.xml'
kubernetes-validations:
name: Kubernetes with Validations
runs-on: ubuntu-latest
needs:
- changes
- build
if: |
(needs.changes.outputs.go == 'true') || ${{ inputs.run_e2e }}
strategy:
matrix:
k8s: [v1.27.1]
steps:
- name: Checkout
uses: actions/checkout@c85c95e3d7251135ab7dc9ce3241c5835cc595a9 # v3.5.3
- name: cache
uses: actions/download-artifact@9bc31d5ccc31df68ecc42ccf4149144866c47d8a # v3.0.2
with:
name: docker.tar.gz
- name: Create Kubernetes ${{ matrix.k8s }} cluster
id: kind
run: |
kind create cluster --image=kindest/node:${{ matrix.k8s }} --config test/e2e/kind.yaml
- name: Load images from cache
run: |
echo "loading docker images..."
pigz -dc docker.tar.gz | docker load
- name: Run e2e tests
env:
KIND_CLUSTER_NAME: kind
SKIP_CLUSTER_CREATION: true
SKIP_IMAGE_CREATION: true
ENABLE_VALIDATIONS: true
run: |
kind get kubeconfig > $HOME/.kube/kind-config-kind
make kind-e2e-test
- name: Upload e2e junit-reports
uses: actions/upload-artifact@0b7f8abb1508181956e8e162db84b466c27e18ce # v3.1.2
if: success() || failure()
with:
name: e2e-test-reports-${{ matrix.k8s }}
path: 'test/junitreports/report*.xml'
kubernetes-chroot:
name: Kubernetes chroot

View file

@ -294,6 +294,7 @@ As of version `1.26.0` of this chart, by simply not providing any clusterIP valu
| controller.dnsConfig | object | `{}` | Optionally customize the pod dnsConfig. |
| controller.dnsPolicy | string | `"ClusterFirst"` | Optionally change this to ClusterFirstWithHostNet in case you have 'hostNetwork: true'. By default, while using host network, name resolution uses the host's DNS. If you wish nginx-controller to keep resolving names inside the k8s network, use ClusterFirstWithHostNet. |
| controller.electionID | string | `""` | Election ID to use for status update, by default it uses the controller name combined with a suffix of 'leader' |
| controller.enableAnnotationValidations | bool | `false` | |
| controller.enableMimalloc | bool | `true` | Enable mimalloc as a drop-in replacement for malloc. # ref: https://github.com/microsoft/mimalloc # |
| controller.enableTopologyAwareRouting | bool | `false` | This configuration enables Topology Aware Routing feature, used together with service annotation service.kubernetes.io/topology-aware-hints="auto" Defaults to false |
| controller.existingPsp | string | `""` | Use an existing PSP instead of creating one |

View file

@ -1,5 +1,8 @@
{{- define "ingress-nginx.params" -}}
- /nginx-ingress-controller
{{- if .Values.controller.enableAnnotationValidations }}
- --enable-annotation-validation=true
{{- end }}
{{- if .Values.defaultBackend.enabled }}
- --default-backend-service=$(POD_NAMESPACE)/{{ include "ingress-nginx.defaultBackend.fullname" . }}
{{- end }}

View file

@ -15,6 +15,7 @@ commonLabels: {}
controller:
name: controller
enableAnnotationValidations: false
image:
## Keep false as default for now!
chroot: false

View file

@ -15,6 +15,7 @@ They are set in the container spec of the `ingress-nginx-controller` Deployment
| `--default-backend-service` | Service used to serve HTTP requests not matching any known server name (catch-all). Takes the form "namespace/name". The controller configures NGINX to forward requests to the first port of this Service. |
| `--default-server-port` | Port to use for exposing the default server (catch-all). (default 8181) |
| `--default-ssl-certificate` | Secret containing a SSL certificate to be used by the default HTTPS server (catch-all). Takes the form "namespace/name". |
| `--enable-annotation-validation` | If true, will enable the annotation validation feature. This value will be defaulted to true on a future release. |
| `--disable-catch-all` | Disable support for catch-all Ingresses. (default false) |
| `--disable-full-test` | Disable full test of all merged ingresses at the admission stage and tests the template of the ingress being created or updated (full test of all ingresses is enabled by default). |
| `--disable-svc-external-name` | Disable support for Services of type ExternalName. (default false) |

View file

@ -29,7 +29,9 @@ The following table shows a configuration option's name, type, and the default v
|:---|:---|:------|:----|
|[add-headers](#add-headers)|string|""||
|[allow-backend-server-header](#allow-backend-server-header)|bool|"false"||
|[allow-cross-namespace-resources](#allow-cross-namespace-resources)|bool|"true"||
|[allow-snippet-annotations](#allow-snippet-annotations)|bool|true||
|[annotations-risk-level](#annotations-risk-level)|string|Critical||
|[annotation-value-word-blocklist](#annotation-value-word-blocklist)|string array|""||
|[hide-headers](#hide-headers)|string array|empty||
|[access-log-params](#access-log-params)|string|""||
@ -239,6 +241,20 @@ Sets custom headers from named configmap before sending traffic to the client. S
Enables the return of the header Server from the backend instead of the generic nginx string. _**default:**_ is disabled
## allow-cross-namespace-resources
Enables users to consume cross namespace resource on annotations, when was previously enabled . _**default:**_ true
**Annotations that may be impacted with this change**:
* `auth-secret`
* `auth-proxy-set-header`
* `auth-tls-secret`
* `fastcgi-params-configmap`
* `proxy-ssl-secret`
**This option will be defaulted to false in the next major release**
## allow-snippet-annotations
Enables Ingress to parse and add *-snippet annotations/directives created by the user. _**default:**_ `true`
@ -246,6 +262,16 @@ Enables Ingress to parse and add *-snippet annotations/directives created by the
Warning: We recommend enabling this option only if you TRUST users with permission to create Ingress objects, as this
may allow a user to add restricted configurations to the final nginx.conf file
**This option will be defaulted to false in the next major release**
## annotations-risk-level
Represents the risk accepted on an annotation. If the risk is, for instance `Medium`, annotations with risk High and Critical will not be accepted.
Accepted values are `Critical`, `High`, `Medium` and `Low`.
Defaults to `Critical` but will be changed to `High` on the next minor release
## annotation-value-word-blocklist
Contains a comma-separated value of chars/words that are well known of being used to abuse Ingress configuration

View file

@ -27,19 +27,44 @@ import (
"k8s.io/ingress-nginx/internal/ingress/resolver"
)
const (
serverAliasAnnotation = "server-alias"
)
var aliasAnnotation = parser.Annotation{
Group: "alias",
Annotations: parser.AnnotationFields{
serverAliasAnnotation: {
Validator: parser.ValidateArrayOfServerName,
Scope: parser.AnnotationScopeIngress,
Risk: parser.AnnotationRiskHigh, // High as this allows regex chars
Documentation: `this annotation can be used to define additional server
aliases for this Ingress`,
},
},
}
type alias struct {
r resolver.Resolver
r resolver.Resolver
annotationConfig parser.Annotation
}
// NewParser creates a new Alias annotation parser
func NewParser(r resolver.Resolver) parser.IngressAnnotation {
return alias{r}
return alias{
r: r,
annotationConfig: aliasAnnotation,
}
}
func (a alias) GetDocumentation() parser.AnnotationFields {
return a.annotationConfig.Annotations
}
// Parse parses the annotations contained in the ingress rule
// used to add an alias to the provided hosts
func (a alias) Parse(ing *networking.Ingress) (interface{}, error) {
val, err := parser.GetStringAnnotation("server-alias", ing)
val, err := parser.GetStringAnnotation(serverAliasAnnotation, ing, a.annotationConfig.Annotations)
if err != nil {
return []string{}, err
}
@ -61,3 +86,8 @@ func (a alias) Parse(ing *networking.Ingress) (interface{}, error) {
return l, nil
}
func (a alias) Validate(anns map[string]string) error {
maxrisk := parser.StringRiskToRisk(a.r.GetSecurityConfiguration().AnnotationsRiskLevel)
return parser.CheckAnnotationRisk(anns, maxrisk, aliasAnnotation.Annotations)
}

View file

@ -27,7 +27,7 @@ import (
"k8s.io/ingress-nginx/internal/ingress/resolver"
)
var annotation = parser.GetAnnotationWithPrefix("server-alias")
var annotation = parser.GetAnnotationWithPrefix(serverAliasAnnotation)
func TestParse(t *testing.T) {
ap := NewParser(&resolver.Mock{})
@ -36,16 +36,20 @@ func TestParse(t *testing.T) {
}
testCases := []struct {
annotations map[string]string
expected []string
annotations map[string]string
expected []string
skipValidation bool
wantErr bool
}{
{map[string]string{annotation: "a.com, b.com, , c.com"}, []string{"a.com", "b.com", "c.com"}},
{map[string]string{annotation: "www.example.com"}, []string{"www.example.com"}},
{map[string]string{annotation: "*.example.com,www.example.*"}, []string{"*.example.com", "www.example.*"}},
{map[string]string{annotation: `~^www\d+\.example\.com$`}, []string{`~^www\d+\.example\.com$`}},
{map[string]string{annotation: ""}, []string{}},
{map[string]string{}, []string{}},
{nil, []string{}},
{map[string]string{annotation: "a.com, b.com, , c.com"}, []string{"a.com", "b.com", "c.com"}, false, false},
{map[string]string{annotation: "www.example.com"}, []string{"www.example.com"}, false, false},
{map[string]string{annotation: "*.example.com,www.example.*"}, []string{"*.example.com", "www.example.*"}, false, false},
{map[string]string{annotation: `~^www\d+\.example\.com$`}, []string{`~^www\d+\.example\.com$`}, false, false},
{map[string]string{annotation: `www.xpto;lala`}, []string{}, false, true},
{map[string]string{annotation: `www.xpto;lala`}, []string{"www.xpto;lala"}, true, false}, // When we skip validation no error should happen
{map[string]string{annotation: ""}, []string{}, false, true},
{map[string]string{}, []string{}, false, true},
{nil, []string{}, false, true},
}
ing := &networking.Ingress{
@ -58,7 +62,16 @@ func TestParse(t *testing.T) {
for _, testCase := range testCases {
ing.SetAnnotations(testCase.annotations)
result, _ := ap.Parse(ing)
if testCase.skipValidation {
parser.EnableAnnotationValidation = false
}
defer func() {
parser.EnableAnnotationValidation = true
}()
result, err := ap.Parse(ing)
if (err != nil) != testCase.wantErr {
t.Errorf("ParseAliasAnnotation() annotation: %s, error = %v, wantErr %v", testCase.annotations, err, testCase.wantErr)
}
if !reflect.DeepEqual(result, testCase.expected) {
t.Errorf("expected %v but returned %v, annotations: %s", testCase.expected, result, testCase.annotations)
}

View file

@ -44,8 +44,8 @@ import (
"k8s.io/ingress-nginx/internal/ingress/annotations/fastcgi"
"k8s.io/ingress-nginx/internal/ingress/annotations/globalratelimit"
"k8s.io/ingress-nginx/internal/ingress/annotations/http2pushpreload"
"k8s.io/ingress-nginx/internal/ingress/annotations/ipallowlist"
"k8s.io/ingress-nginx/internal/ingress/annotations/ipdenylist"
"k8s.io/ingress-nginx/internal/ingress/annotations/ipwhitelist"
"k8s.io/ingress-nginx/internal/ingress/annotations/loadbalancing"
"k8s.io/ingress-nginx/internal/ingress/annotations/log"
"k8s.io/ingress-nginx/internal/ingress/annotations/mirror"
@ -109,7 +109,6 @@ type Ingress struct {
UpstreamHashBy upstreamhashby.Config
LoadBalancing string
UpstreamVhost string
Whitelist ipwhitelist.SourceRange
Denylist ipdenylist.SourceRange
XForwardedPrefix string
SSLCipher sslcipher.Config
@ -117,6 +116,7 @@ type Ingress struct {
ModSecurity modsecurity.Config
Mirror mirror.Config
StreamSnippet string
Allowlist ipallowlist.SourceRange
}
// Extractor defines the annotation parsers to be used in the extraction of annotations
@ -159,7 +159,7 @@ func NewAnnotationExtractor(cfg resolver.Resolver) Extractor {
"UpstreamHashBy": upstreamhashby.NewParser(cfg),
"LoadBalancing": loadbalancing.NewParser(cfg),
"UpstreamVhost": upstreamvhost.NewParser(cfg),
"Whitelist": ipwhitelist.NewParser(cfg),
"Allowlist": ipallowlist.NewParser(cfg),
"Denylist": ipdenylist.NewParser(cfg),
"XForwardedPrefix": xforwardedprefix.NewParser(cfg),
"SSLCipher": sslcipher.NewParser(cfg),
@ -173,16 +173,23 @@ func NewAnnotationExtractor(cfg resolver.Resolver) Extractor {
}
// Extract extracts the annotations from an Ingress
func (e Extractor) Extract(ing *networking.Ingress) *Ingress {
func (e Extractor) Extract(ing *networking.Ingress) (*Ingress, error) {
pia := &Ingress{
ObjectMeta: ing.ObjectMeta,
}
data := make(map[string]interface{})
for name, annotationParser := range e.annotations {
if err := annotationParser.Validate(ing.GetAnnotations()); err != nil {
return nil, errors.NewRiskyAnnotations(name)
}
val, err := annotationParser.Parse(ing)
klog.V(5).InfoS("Parsing Ingress annotation", "name", name, "ingress", klog.KObj(ing), "value", val)
if err != nil {
if errors.IsValidationError(err) {
klog.ErrorS(err, "ingress contains invalid annotation value")
return nil, err
}
if errors.IsMissingAnnotations(err) {
continue
}
@ -220,5 +227,5 @@ func (e Extractor) Extract(ing *networking.Ingress) *Ingress {
klog.ErrorS(err, "unexpected error merging extracted annotations")
}
return pia
return pia, nil
}

View file

@ -134,8 +134,11 @@ func TestSSLPassthrough(t *testing.T) {
for _, foo := range fooAnns {
ing.SetAnnotations(foo.annotations)
r := ec.Extract(ing).SSLPassthrough
if r != foo.er {
r, err := ec.Extract(ing)
if err != nil {
t.Errorf("Errors should be null: %v", err)
}
if r.SSLPassthrough != foo.er {
t.Errorf("Returned %v but expected %v", r, foo.er)
}
}
@ -158,8 +161,11 @@ func TestUpstreamHashBy(t *testing.T) {
for _, foo := range fooAnns {
ing.SetAnnotations(foo.annotations)
r := ec.Extract(ing).UpstreamHashBy.UpstreamHashBy
if r != foo.er {
r, err := ec.Extract(ing)
if err != nil {
t.Errorf("error should be null: %v", err)
}
if r.UpstreamHashBy.UpstreamHashBy != foo.er {
t.Errorf("Returned %v but expected %v", r, foo.er)
}
}
@ -185,7 +191,11 @@ func TestAffinitySession(t *testing.T) {
for _, foo := range fooAnns {
ing.SetAnnotations(foo.annotations)
r := ec.Extract(ing).SessionAffinity
rann, err := ec.Extract(ing)
if err != nil {
t.Errorf("error should be null: %v", err)
}
r := rann.SessionAffinity
t.Logf("Testing pass %v %v", foo.affinitytype, foo.cookiename)
if r.Type != foo.affinitytype {
@ -228,7 +238,11 @@ func TestCors(t *testing.T) {
for _, foo := range fooAnns {
ing.SetAnnotations(foo.annotations)
r := ec.Extract(ing).CorsConfig
rann, err := ec.Extract(ing)
if err != nil {
t.Errorf("error should be null: %v", err)
}
r := rann.CorsConfig
t.Logf("Testing pass %v %v %v %v %v", foo.corsenabled, foo.methods, foo.headers, foo.origin, foo.credentials)
if r.CorsEnabled != foo.corsenabled {
@ -277,7 +291,11 @@ func TestCustomHTTPErrors(t *testing.T) {
for _, foo := range fooAnns {
ing.SetAnnotations(foo.annotations)
r := ec.Extract(ing).CustomHTTPErrors
rann, err := ec.Extract(ing)
if err != nil {
t.Errorf("error should be null: %v", err)
}
r := rann.CustomHTTPErrors
// Check that expected codes were created
for i := range foo.er {

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)
}

View file

@ -26,6 +26,7 @@ import (
api "k8s.io/api/core/v1"
networking "k8s.io/api/networking/v1"
meta_v1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/client-go/tools/cache"
"k8s.io/ingress-nginx/internal/ingress/annotations/parser"
ing_errors "k8s.io/ingress-nginx/internal/ingress/errors"
"k8s.io/ingress-nginx/internal/ingress/resolver"
@ -79,13 +80,18 @@ type mockSecret struct {
}
func (m mockSecret) GetSecret(name string) (*api.Secret, error) {
if name != "default/demo-secret" {
if name != "default/demo-secret" && name != "otherns/demo-secret" {
return nil, fmt.Errorf("there is no secret with name %v", name)
}
ns, _, err := cache.SplitMetaNamespaceKey(name)
if err != nil {
return nil, err
}
return &api.Secret{
ObjectMeta: meta_v1.ObjectMeta{
Namespace: api.NamespaceDefault,
Namespace: ns,
Name: "demo-secret",
},
Data: map[string][]byte{"auth": []byte("foo:$apr1$OFG3Xybp$ckL0FHDAkoXYIlH9.cysT0")},
@ -106,13 +112,91 @@ func TestIngressAuthBadAuthType(t *testing.T) {
ing := buildIngress()
data := map[string]string{}
data[parser.GetAnnotationWithPrefix("auth-type")] = "invalid"
data[parser.GetAnnotationWithPrefix(authTypeAnnotation)] = "invalid"
ing.SetAnnotations(data)
_, dir, _ := dummySecretContent(t)
defer os.RemoveAll(dir)
expected := ing_errors.NewLocationDenied("invalid authentication type")
expected := ing_errors.NewValidationError("nginx.ingress.kubernetes.io/auth-type")
_, err := NewParser(dir, &mockSecret{}).Parse(ing)
if err.Error() != expected.Error() {
t.Errorf("expected '%v' but got '%v'", expected, err)
}
}
func TestIngressInvalidRealm(t *testing.T) {
ing := buildIngress()
data := map[string]string{}
data[parser.GetAnnotationWithPrefix(authTypeAnnotation)] = "basic"
data[parser.GetAnnotationWithPrefix(authRealmAnnotation)] = "something weird ; location trying to { break }"
data[parser.GetAnnotationWithPrefix(AuthSecretAnnotation)] = "demo-secret"
ing.SetAnnotations(data)
_, dir, _ := dummySecretContent(t)
defer os.RemoveAll(dir)
expected := ing_errors.NewValidationError("nginx.ingress.kubernetes.io/auth-realm")
_, err := NewParser(dir, &mockSecret{}).Parse(ing)
if err.Error() != expected.Error() {
t.Errorf("expected '%v' but got '%v'", expected, err)
}
}
func TestIngressInvalidDifferentNamespace(t *testing.T) {
ing := buildIngress()
data := map[string]string{}
data[parser.GetAnnotationWithPrefix(authTypeAnnotation)] = "basic"
data[parser.GetAnnotationWithPrefix(AuthSecretAnnotation)] = "otherns/demo-secret"
ing.SetAnnotations(data)
_, dir, _ := dummySecretContent(t)
defer os.RemoveAll(dir)
expected := ing_errors.LocationDenied{
Reason: errors.New("cross namespace usage of secrets is not allowed"),
}
_, err := NewParser(dir, &mockSecret{}).Parse(ing)
if err.Error() != expected.Error() {
t.Errorf("expected '%v' but got '%v'", expected, err)
}
}
func TestIngressInvalidDifferentNamespaceAllowed(t *testing.T) {
ing := buildIngress()
data := map[string]string{}
data[parser.GetAnnotationWithPrefix(authTypeAnnotation)] = "basic"
data[parser.GetAnnotationWithPrefix(AuthSecretAnnotation)] = "otherns/demo-secret"
ing.SetAnnotations(data)
_, dir, _ := dummySecretContent(t)
defer os.RemoveAll(dir)
r := mockSecret{}
r.AllowCrossNamespace = true
_, err := NewParser(dir, r).Parse(ing)
if err != nil {
t.Errorf("not expecting an error")
}
}
func TestIngressInvalidSecretName(t *testing.T) {
ing := buildIngress()
data := map[string]string{}
data[parser.GetAnnotationWithPrefix(authTypeAnnotation)] = "basic"
data[parser.GetAnnotationWithPrefix(AuthSecretAnnotation)] = "demo-secret;xpto"
ing.SetAnnotations(data)
_, dir, _ := dummySecretContent(t)
defer os.RemoveAll(dir)
expected := ing_errors.LocationDenied{
Reason: errors.New("error reading secret name from annotation: annotation nginx.ingress.kubernetes.io/auth-secret contains invalid value"),
}
_, err := NewParser(dir, &mockSecret{}).Parse(ing)
if err.Error() != expected.Error() {
t.Errorf("expected '%v' but got '%v'", expected, err)
@ -123,7 +207,7 @@ func TestInvalidIngressAuthNoSecret(t *testing.T) {
ing := buildIngress()
data := map[string]string{}
data[parser.GetAnnotationWithPrefix("auth-type")] = "basic"
data[parser.GetAnnotationWithPrefix(authTypeAnnotation)] = "basic"
ing.SetAnnotations(data)
_, dir, _ := dummySecretContent(t)
@ -142,9 +226,9 @@ func TestIngressAuth(t *testing.T) {
ing := buildIngress()
data := map[string]string{}
data[parser.GetAnnotationWithPrefix("auth-type")] = "basic"
data[parser.GetAnnotationWithPrefix("auth-secret")] = "demo-secret"
data[parser.GetAnnotationWithPrefix("auth-realm")] = "-realm-"
data[parser.GetAnnotationWithPrefix(authTypeAnnotation)] = "basic"
data[parser.GetAnnotationWithPrefix(AuthSecretAnnotation)] = "demo-secret"
data[parser.GetAnnotationWithPrefix(authRealmAnnotation)] = "-realm-"
ing.SetAnnotations(data)
_, dir, _ := dummySecretContent(t)
@ -173,9 +257,9 @@ func TestIngressAuthWithoutSecret(t *testing.T) {
ing := buildIngress()
data := map[string]string{}
data[parser.GetAnnotationWithPrefix("auth-type")] = "basic"
data[parser.GetAnnotationWithPrefix("auth-secret")] = "invalid-secret"
data[parser.GetAnnotationWithPrefix("auth-realm")] = "-realm-"
data[parser.GetAnnotationWithPrefix(authTypeAnnotation)] = "basic"
data[parser.GetAnnotationWithPrefix(AuthSecretAnnotation)] = "invalid-secret"
data[parser.GetAnnotationWithPrefix(authRealmAnnotation)] = "-realm-"
ing.SetAnnotations(data)
_, dir, _ := dummySecretContent(t)
@ -191,10 +275,10 @@ func TestIngressAuthInvalidSecretKey(t *testing.T) {
ing := buildIngress()
data := map[string]string{}
data[parser.GetAnnotationWithPrefix("auth-type")] = "basic"
data[parser.GetAnnotationWithPrefix("auth-secret")] = "demo-secret"
data[parser.GetAnnotationWithPrefix("auth-secret-type")] = "invalid-type"
data[parser.GetAnnotationWithPrefix("auth-realm")] = "-realm-"
data[parser.GetAnnotationWithPrefix(authTypeAnnotation)] = "basic"
data[parser.GetAnnotationWithPrefix(AuthSecretAnnotation)] = "demo-secret"
data[parser.GetAnnotationWithPrefix(authSecretTypeAnnotation)] = "invalid-type"
data[parser.GetAnnotationWithPrefix(authRealmAnnotation)] = "-realm-"
ing.SetAnnotations(data)
_, dir, _ := dummySecretContent(t)

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)

View file

@ -23,23 +23,52 @@ import (
"k8s.io/ingress-nginx/internal/ingress/resolver"
)
const (
enableGlobalAuthAnnotation = "enable-global-auth"
)
var globalAuthAnnotations = parser.Annotation{
Group: "authentication",
Annotations: parser.AnnotationFields{
enableGlobalAuthAnnotation: {
Validator: parser.ValidateBool,
Scope: parser.AnnotationScopeLocation,
Risk: parser.AnnotationRiskLow,
Documentation: `Defines if the global external authentication should be enabled.`,
},
},
}
type authReqGlobal 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 authReqGlobal{r}
return authReqGlobal{
r: r,
annotationConfig: globalAuthAnnotations,
}
}
// ParseAnnotations parses the annotations contained in the ingress
// rule used to enable or disable global external authentication
func (a authReqGlobal) Parse(ing *networking.Ingress) (interface{}, error) {
enableGlobalAuth, err := parser.GetBoolAnnotation("enable-global-auth", ing)
enableGlobalAuth, err := parser.GetBoolAnnotation(enableGlobalAuthAnnotation, ing, a.annotationConfig.Annotations)
if err != nil {
enableGlobalAuth = true
}
return enableGlobalAuth, nil
}
func (a authReqGlobal) GetDocumentation() parser.AnnotationFields {
return a.annotationConfig.Annotations
}
func (a authReqGlobal) Validate(anns map[string]string) error {
maxrisk := parser.StringRiskToRisk(a.r.GetSecurityConfiguration().AnnotationsRiskLevel)
return parser.CheckAnnotationRisk(anns, maxrisk, globalAuthAnnotations.Annotations)
}

View file

@ -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)
}

View file

@ -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)

View file

@ -17,49 +17,72 @@ limitations under the License.
package backendprotocol
import (
"regexp"
"strings"
networking "k8s.io/api/networking/v1"
"k8s.io/klog/v2"
"k8s.io/ingress-nginx/internal/ingress/annotations/parser"
"k8s.io/ingress-nginx/internal/ingress/errors"
"k8s.io/ingress-nginx/internal/ingress/resolver"
)
// HTTP protocol
const HTTP = "HTTP"
var (
validProtocols = regexp.MustCompile(`^(AUTO_HTTP|HTTP|HTTPS|GRPC|GRPCS|FCGI)$`)
validProtocols = []string{"auto_http", "http", "https", "grpc", "grpcs", "fcgi"}
)
const (
http = "HTTP"
backendProtocolAnnotation = "backend-protocol"
)
var backendProtocolConfig = parser.Annotation{
Group: "backend",
Annotations: parser.AnnotationFields{
backendProtocolAnnotation: {
Validator: parser.ValidateOptions(validProtocols, false, true),
Scope: parser.AnnotationScopeLocation,
Risk: parser.AnnotationRiskLow, // Low, as it allows just a set of options
Documentation: `this annotation can be used to define which protocol should
be used to communicate with backends`,
},
},
}
type backendProtocol struct {
r resolver.Resolver
r resolver.Resolver
annotationConfig parser.Annotation
}
// NewParser creates a new backend protocol annotation parser
func NewParser(r resolver.Resolver) parser.IngressAnnotation {
return backendProtocol{r}
return backendProtocol{
r: r,
annotationConfig: backendProtocolConfig,
}
}
func (a backendProtocol) GetDocumentation() parser.AnnotationFields {
return a.annotationConfig.Annotations
}
// ParseAnnotations parses the annotations contained in the ingress
// rule used to indicate the backend protocol.
func (a backendProtocol) Parse(ing *networking.Ingress) (interface{}, error) {
if ing.GetAnnotations() == nil {
return HTTP, nil
return http, nil
}
proto, err := parser.GetStringAnnotation("backend-protocol", ing)
proto, err := parser.GetStringAnnotation(backendProtocolAnnotation, ing, a.annotationConfig.Annotations)
if err != nil {
return HTTP, nil
}
proto = strings.TrimSpace(strings.ToUpper(proto))
if !validProtocols.MatchString(proto) {
klog.Warningf("Protocol %v is not a valid value for the backend-protocol annotation. Using HTTP as protocol", proto)
return HTTP, nil
if errors.IsValidationError(err) {
klog.Warningf("validation error %s. Using HTTP as protocol", err)
}
return http, nil
}
return proto, nil
}
func (a backendProtocol) Validate(anns map[string]string) error {
maxrisk := parser.StringRiskToRisk(a.r.GetSecurityConfiguration().AnnotationsRiskLevel)
return parser.CheckAnnotationRisk(anns, maxrisk, backendProtocolConfig.Annotations)
}

View file

@ -77,7 +77,7 @@ func TestParseInvalidAnnotations(t *testing.T) {
}
// Test invalid annotation set
data[parser.GetAnnotationWithPrefix("backend-protocol")] = "INVALID"
data[parser.GetAnnotationWithPrefix(backendProtocolAnnotation)] = "INVALID"
ing.SetAnnotations(data)
i, err = NewParser(&resolver.Mock{}).Parse(ing)
@ -97,7 +97,7 @@ func TestParseAnnotations(t *testing.T) {
ing := buildIngress()
data := map[string]string{}
data[parser.GetAnnotationWithPrefix("backend-protocol")] = "HTTPS"
data[parser.GetAnnotationWithPrefix(backendProtocolAnnotation)] = " HTTPS "
ing.SetAnnotations(data)
i, err := NewParser(&resolver.Mock{}).Parse(ing)

View file

@ -18,14 +18,82 @@ package canary
import (
networking "k8s.io/api/networking/v1"
"k8s.io/klog/v2"
"k8s.io/ingress-nginx/internal/ingress/annotations/parser"
"k8s.io/ingress-nginx/internal/ingress/errors"
"k8s.io/ingress-nginx/internal/ingress/resolver"
)
const (
canaryAnnotation = "canary"
canaryWeightAnnotation = "canary-weight"
canaryWeightTotalAnnotation = "canary-weight-total"
canaryByHeaderAnnotation = "canary-by-header"
canaryByHeaderValueAnnotation = "canary-by-header-value"
canaryByHeaderPatternAnnotation = "canary-by-header-pattern"
canaryByCookieAnnotation = "canary-by-cookie"
)
var CanaryAnnotations = parser.Annotation{
Group: "canary",
Annotations: parser.AnnotationFields{
canaryAnnotation: {
Validator: parser.ValidateBool,
Scope: parser.AnnotationScopeIngress,
Risk: parser.AnnotationRiskLow,
Documentation: `This annotation enables the Ingress spec to act as an alternative service for requests to route to depending on the rules applied`,
},
canaryWeightAnnotation: {
Validator: parser.ValidateInt,
Scope: parser.AnnotationScopeIngress,
Risk: parser.AnnotationRiskLow,
Documentation: `This annotation defines the integer based (0 - ) percent of random requests that should be routed to the service specified in the canary Ingress`,
},
canaryWeightTotalAnnotation: {
Validator: parser.ValidateInt,
Scope: parser.AnnotationScopeIngress,
Risk: parser.AnnotationRiskLow,
Documentation: `This annotation The total weight of traffic. If unspecified, it defaults to 100`,
},
canaryByHeaderAnnotation: {
Validator: parser.ValidateRegex(*parser.BasicCharsRegex, true),
Scope: parser.AnnotationScopeIngress,
Risk: parser.AnnotationRiskMedium,
Documentation: `This annotation defines the header that should be used for notifying the Ingress to route the request to the service specified in the Canary Ingress.
When the request header is set to 'always', it will be routed to the canary. When the header is set to 'never', it will never be routed to the canary.
For any other value, the header will be ignored and the request compared against the other canary rules by precedence`,
},
canaryByHeaderValueAnnotation: {
Validator: parser.ValidateRegex(*parser.BasicCharsRegex, true),
Scope: parser.AnnotationScopeIngress,
Risk: parser.AnnotationRiskMedium,
Documentation: `This annotation defines the header value to match for notifying the Ingress to route the request to the service specified in the Canary Ingress.
When the request header is set to this value, it will be routed to the canary. For any other header value, the header will be ignored and the request compared against the other canary rules by precedence.
This annotation has to be used together with 'canary-by-header'. The annotation is an extension of the 'canary-by-header' to allow customizing the header value instead of using hardcoded values.
It doesn't have any effect if the 'canary-by-header' annotation is not defined`,
},
canaryByHeaderPatternAnnotation: {
Validator: parser.ValidateRegex(*parser.IsValidRegex, false),
Scope: parser.AnnotationScopeIngress,
Risk: parser.AnnotationRiskMedium,
Documentation: `This annotation works the same way as canary-by-header-value except it does PCRE Regex matching.
Note that when 'canary-by-header-value' is set this annotation will be ignored.
When the given Regex causes error during request processing, the request will be considered as not matching.`,
},
canaryByCookieAnnotation: {
Validator: parser.ValidateRegex(*parser.BasicCharsRegex, true),
Scope: parser.AnnotationScopeIngress,
Risk: parser.AnnotationRiskMedium,
Documentation: `This annotation defines the cookie that should be used for notifying the Ingress to route the request to the service specified in the Canary Ingress.
When the cookie is set to 'always', it will be routed to the canary. When the cookie is set to 'never', it will never be routed to the canary`,
},
},
}
type canary struct {
r resolver.Resolver
r resolver.Resolver
annotationConfig parser.Annotation
}
// Config returns the configuration rules for setting up the Canary
@ -41,7 +109,10 @@ type Config struct {
// NewParser parses the ingress for canary related annotations
func NewParser(r resolver.Resolver) parser.IngressAnnotation {
return canary{r}
return canary{
r: r,
annotationConfig: CanaryAnnotations,
}
}
// Parse parses the annotations contained in the ingress
@ -50,45 +121,75 @@ func (c canary) Parse(ing *networking.Ingress) (interface{}, error) {
config := &Config{}
var err error
config.Enabled, err = parser.GetBoolAnnotation("canary", ing)
config.Enabled, err = parser.GetBoolAnnotation(canaryAnnotation, ing, c.annotationConfig.Annotations)
if err != nil {
if errors.IsValidationError(err) {
klog.Warningf("%s is invalid, defaulting to 'false'", canaryAnnotation)
}
config.Enabled = false
}
config.Weight, err = parser.GetIntAnnotation("canary-weight", ing)
config.Weight, err = parser.GetIntAnnotation(canaryWeightAnnotation, ing, c.annotationConfig.Annotations)
if err != nil {
if errors.IsValidationError(err) {
klog.Warningf("%s is invalid, defaulting to '0'", canaryWeightAnnotation)
}
config.Weight = 0
}
config.WeightTotal, err = parser.GetIntAnnotation("canary-weight-total", ing)
config.WeightTotal, err = parser.GetIntAnnotation(canaryWeightTotalAnnotation, ing, c.annotationConfig.Annotations)
if err != nil {
if errors.IsValidationError(err) {
klog.Warningf("%s is invalid, defaulting to '100'", canaryWeightTotalAnnotation)
}
config.WeightTotal = 100
}
config.Header, err = parser.GetStringAnnotation("canary-by-header", ing)
config.Header, err = parser.GetStringAnnotation(canaryByHeaderAnnotation, ing, c.annotationConfig.Annotations)
if err != nil {
if errors.IsValidationError(err) {
klog.Warningf("%s is invalid, defaulting to ''", canaryByHeaderAnnotation)
}
config.Header = ""
}
config.HeaderValue, err = parser.GetStringAnnotation("canary-by-header-value", ing)
config.HeaderValue, err = parser.GetStringAnnotation(canaryByHeaderValueAnnotation, ing, c.annotationConfig.Annotations)
if err != nil {
if errors.IsValidationError(err) {
klog.Warningf("%s is invalid, defaulting to ''", canaryByHeaderValueAnnotation)
}
config.HeaderValue = ""
}
config.HeaderPattern, err = parser.GetStringAnnotation("canary-by-header-pattern", ing)
config.HeaderPattern, err = parser.GetStringAnnotation(canaryByHeaderPatternAnnotation, ing, c.annotationConfig.Annotations)
if err != nil {
if errors.IsValidationError(err) {
klog.Warningf("%s is invalid, defaulting to ''", canaryByHeaderPatternAnnotation)
}
config.HeaderPattern = ""
}
config.Cookie, err = parser.GetStringAnnotation("canary-by-cookie", ing)
config.Cookie, err = parser.GetStringAnnotation(canaryByCookieAnnotation, ing, c.annotationConfig.Annotations)
if err != nil {
if errors.IsValidationError(err) {
klog.Warningf("%s is invalid, defaulting to ''", canaryByCookieAnnotation)
}
config.Cookie = ""
}
if !config.Enabled && (config.Weight > 0 || len(config.Header) > 0 || len(config.HeaderValue) > 0 || len(config.Cookie) > 0 ||
len(config.HeaderPattern) > 0) {
return nil, errors.NewInvalidAnnotationConfiguration("canary", "configured but not enabled")
return nil, errors.NewInvalidAnnotationConfiguration(canaryAnnotation, "configured but not enabled")
}
return config, nil
}
func (c canary) GetDocumentation() parser.AnnotationFields {
return c.annotationConfig.Annotations
}
func (a canary) Validate(anns map[string]string) error {
maxrisk := parser.StringRiskToRisk(a.r.GetSecurityConfiguration().AnnotationsRiskLevel)
return parser.CheckAnnotationRisk(anns, maxrisk, CanaryAnnotations.Annotations)
}

View file

@ -23,17 +23,49 @@ import (
"k8s.io/ingress-nginx/internal/ingress/resolver"
)
const (
clientBodyBufferSizeAnnotation = "client-body-buffer-size"
)
var clientBodyBufferSizeConfig = parser.Annotation{
Group: "backend",
Annotations: parser.AnnotationFields{
clientBodyBufferSizeAnnotation: {
Validator: parser.ValidateRegex(*parser.SizeRegex, true),
Scope: parser.AnnotationScopeLocation,
Risk: parser.AnnotationRiskLow, // Low, as it allows just a set of options
Documentation: `Sets buffer size for reading client request body per location.
In case the request body is larger than the buffer, the whole body or only its part is written to a temporary file.
By default, buffer size is equal to two memory pages. This is 8K on x86, other 32-bit platforms, and x86-64.
It is usually 16K on other 64-bit platforms. This annotation is applied to each location provided in the ingress rule.`,
},
},
}
type clientBodyBufferSize struct {
r resolver.Resolver
r resolver.Resolver
annotationConfig parser.Annotation
}
// NewParser creates a new clientBodyBufferSize annotation parser
func NewParser(r resolver.Resolver) parser.IngressAnnotation {
return clientBodyBufferSize{r}
return clientBodyBufferSize{
r: r,
annotationConfig: clientBodyBufferSizeConfig,
}
}
func (cbbs clientBodyBufferSize) GetDocumentation() parser.AnnotationFields {
return cbbs.annotationConfig.Annotations
}
// Parse parses the annotations contained in the ingress rule
// used to add an client-body-buffer-size to the provided locations
func (cbbs clientBodyBufferSize) Parse(ing *networking.Ingress) (interface{}, error) {
return parser.GetStringAnnotation("client-body-buffer-size", ing)
return parser.GetStringAnnotation(clientBodyBufferSizeAnnotation, ing, cbbs.annotationConfig.Annotations)
}
func (a clientBodyBufferSize) Validate(anns map[string]string) error {
maxrisk := parser.StringRiskToRisk(a.r.GetSecurityConfiguration().AnnotationsRiskLevel)
return parser.CheckAnnotationRisk(anns, maxrisk, clientBodyBufferSizeConfig.Annotations)
}

View file

@ -39,6 +39,9 @@ func TestParse(t *testing.T) {
}{
{map[string]string{annotation: "8k"}, "8k"},
{map[string]string{annotation: "16k"}, "16k"},
{map[string]string{annotation: "10000"}, "10000"},
{map[string]string{annotation: "16R"}, ""},
{map[string]string{annotation: "16kkk"}, ""},
{map[string]string{annotation: ""}, ""},
{map[string]string{}, ""},
{nil, ""},

View file

@ -17,12 +17,34 @@ limitations under the License.
package connection
import (
"regexp"
networking "k8s.io/api/networking/v1"
"k8s.io/ingress-nginx/internal/ingress/annotations/parser"
"k8s.io/ingress-nginx/internal/ingress/resolver"
)
const (
connectionProxyHeaderAnnotation = "connection-proxy-header"
)
var (
validConnectionHeaderValue = regexp.MustCompile(`^(close|keep-alive)$`)
)
var connectionHeadersAnnotations = parser.Annotation{
Group: "backend",
Annotations: parser.AnnotationFields{
connectionProxyHeaderAnnotation: {
Validator: parser.ValidateRegex(*validConnectionHeaderValue, true),
Scope: parser.AnnotationScopeLocation,
Risk: parser.AnnotationRiskLow,
Documentation: `This annotation allows setting a specific value for "proxy_set_header Connection" directive. Right now it is restricted to "close" or "keep-alive"`,
},
},
}
// Config returns the connection header configuration for an Ingress rule
type Config struct {
Header string `json:"header"`
@ -30,18 +52,22 @@ type Config struct {
}
type connection struct {
r resolver.Resolver
r resolver.Resolver
annotationConfig parser.Annotation
}
// NewParser creates a new port in redirect annotation parser
func NewParser(r resolver.Resolver) parser.IngressAnnotation {
return connection{r}
return connection{
r: r,
annotationConfig: connectionHeadersAnnotations,
}
}
// Parse parses the annotations contained in the ingress
// rule used to indicate if the connection header should be overridden.
func (a connection) Parse(ing *networking.Ingress) (interface{}, error) {
cp, err := parser.GetStringAnnotation("connection-proxy-header", ing)
cp, err := parser.GetStringAnnotation(connectionProxyHeaderAnnotation, ing, a.annotationConfig.Annotations)
if err != nil {
return &Config{
Enabled: false,
@ -70,3 +96,12 @@ func (r1 *Config) Equal(r2 *Config) bool {
return true
}
func (a connection) GetDocumentation() parser.AnnotationFields {
return a.annotationConfig.Annotations
}
func (a connection) Validate(anns map[string]string) error {
maxrisk := parser.StringRiskToRisk(a.r.GetSecurityConfiguration().AnnotationsRiskLevel)
return parser.CheckAnnotationRisk(anns, maxrisk, connectionHeadersAnnotations.Annotations)
}

View file

@ -37,10 +37,12 @@ func TestParse(t *testing.T) {
testCases := []struct {
annotations map[string]string
expected *Config
expectErr bool
}{
{map[string]string{annotation: "keep-alive"}, &Config{Enabled: true, Header: "keep-alive"}},
{map[string]string{}, &Config{Enabled: false}},
{nil, &Config{Enabled: false}},
{map[string]string{annotation: "keep-alive"}, &Config{Enabled: true, Header: "keep-alive"}, false},
{map[string]string{annotation: "not-allowed-value"}, &Config{Enabled: false}, true},
{map[string]string{}, &Config{Enabled: false}, true},
{nil, &Config{Enabled: false}, true},
}
ing := &networking.Ingress{
@ -53,11 +55,17 @@ func TestParse(t *testing.T) {
for _, testCase := range testCases {
ing.SetAnnotations(testCase.annotations)
i, _ := ap.Parse(ing)
p, _ := i.(*Config)
i, err := ap.Parse(ing)
if (err != nil) != testCase.expectErr {
t.Fatalf("expected error: %t got error: %t err value: %s. %+v", testCase.expectErr, err != nil, err, testCase.annotations)
}
p, ok := i.(*Config)
if !ok {
t.Fatalf("expected a Config type")
}
if !p.Equal(testCase.expected) {
t.Errorf("expected %v but returned %v, annotations: %s", testCase.expected, p, testCase.annotations)
}
}
}

View file

@ -24,6 +24,7 @@ import (
"k8s.io/klog/v2"
"k8s.io/ingress-nginx/internal/ingress/annotations/parser"
"k8s.io/ingress-nginx/internal/ingress/errors"
"k8s.io/ingress-nginx/internal/ingress/resolver"
)
@ -38,20 +39,87 @@ var (
// Regex are defined here to prevent information leak, if user tries to set anything not valid
// that could cause the Response to contain some internal value/variable (like returning $pid, $upstream_addr, etc)
// Origin must contain a http/s Origin (including or not the port) or the value '*'
// This Regex is composed of the following:
// * Sets a group that can be (https?://)?*?.something.com:port?
// * Allows this to be repeated as much as possible, and separated by comma
// Otherwise it should be '*'
corsOriginRegexValidator = regexp.MustCompile(`^((((https?://)?(\*\.)?[A-Za-z0-9\-\.]*(:[0-9]+)?,?)+)|\*)?$`)
// corsOriginRegex defines the regex for validation inside Parse
corsOriginRegex = regexp.MustCompile(`^(https?://(\*\.)?[A-Za-z0-9\-\.]*(:[0-9]+)?|\*)?$`)
// Method must contain valid methods list (PUT, GET, POST, BLA)
// May contain or not spaces between each verb
corsMethodsRegex = regexp.MustCompile(`^([A-Za-z]+,?\s?)+$`)
// Headers must contain valid values only (X-HEADER12, X-ABC)
// May contain or not spaces between each Header
corsHeadersRegex = regexp.MustCompile(`^([A-Za-z0-9\-\_]+,?\s?)+$`)
// Expose Headers must contain valid values only (*, X-HEADER12, X-ABC)
// May contain or not spaces between each Header
corsExposeHeadersRegex = regexp.MustCompile(`^(([A-Za-z0-9\-\_]+|\*),?\s?)+$`)
)
const (
corsEnableAnnotation = "enable-cors"
corsAllowOriginAnnotation = "cors-allow-origin"
corsAllowHeadersAnnotation = "cors-allow-headers"
corsAllowMethodsAnnotation = "cors-allow-methods"
corsAllowCredentialsAnnotation = "cors-allow-credentials" //#nosec G101
corsExposeHeadersAnnotation = "cors-expose-headers"
corsMaxAgeAnnotation = "cors-max-age"
)
var corsAnnotation = parser.Annotation{
Group: "cors",
Annotations: parser.AnnotationFields{
corsEnableAnnotation: {
Validator: parser.ValidateBool,
Scope: parser.AnnotationScopeIngress,
Risk: parser.AnnotationRiskLow,
Documentation: `This annotation enables Cross-Origin Resource Sharing (CORS) in an Ingress rule`,
},
corsAllowOriginAnnotation: {
Validator: parser.ValidateRegex(*corsOriginRegexValidator, true),
Scope: parser.AnnotationScopeIngress,
Risk: parser.AnnotationRiskMedium,
Documentation: `This annotation controls what's the accepted Origin for CORS.
This is a multi-valued field, separated by ','. It must follow this format: http(s)://origin-site.com or http(s)://origin-site.com:port
It also supports single level wildcard subdomains and follows this format: http(s)://*.foo.bar, http(s)://*.bar.foo:8080 or http(s)://*.abc.bar.foo:9000`,
},
corsAllowHeadersAnnotation: {
Validator: parser.ValidateRegex(*parser.HeadersVariable, true),
Scope: parser.AnnotationScopeIngress,
Risk: parser.AnnotationRiskMedium,
Documentation: `This annotation controls which headers are accepted.
This is a multi-valued field, separated by ',' and accepts letters, numbers, _ and -`,
},
corsAllowMethodsAnnotation: {
Validator: parser.ValidateRegex(*corsMethodsRegex, true),
Scope: parser.AnnotationScopeIngress,
Risk: parser.AnnotationRiskMedium,
Documentation: `This annotation controls which methods are accepted.
This is a multi-valued field, separated by ',' and accepts only letters (upper and lower case)`,
},
corsAllowCredentialsAnnotation: {
Validator: parser.ValidateBool,
Scope: parser.AnnotationScopeIngress,
Risk: parser.AnnotationRiskLow,
Documentation: `This annotation controls if credentials can be passed during CORS operations.`,
},
corsExposeHeadersAnnotation: {
Validator: parser.ValidateRegex(*corsExposeHeadersRegex, true),
Scope: parser.AnnotationScopeIngress,
Risk: parser.AnnotationRiskMedium,
Documentation: `This annotation controls which headers are exposed to response.
This is a multi-valued field, separated by ',' and accepts letters, numbers, _, - and *.`,
},
corsMaxAgeAnnotation: {
Validator: parser.ValidateInt,
Scope: parser.AnnotationScopeIngress,
Risk: parser.AnnotationRiskLow,
Documentation: `This annotation controls how long, in seconds, preflight requests can be cached.`,
},
},
}
type cors struct {
r resolver.Resolver
r resolver.Resolver
annotationConfig parser.Annotation
}
// Config contains the Cors configuration to be used in the Ingress
@ -67,7 +135,10 @@ type Config struct {
// NewParser creates a new CORS annotation parser
func NewParser(r resolver.Resolver) parser.IngressAnnotation {
return cors{r}
return cors{
r: r,
annotationConfig: corsAnnotation,
}
}
// Equal tests for equality between two External types
@ -116,13 +187,16 @@ func (c cors) Parse(ing *networking.Ingress) (interface{}, error) {
var err error
config := &Config{}
config.CorsEnabled, err = parser.GetBoolAnnotation("enable-cors", ing)
config.CorsEnabled, err = parser.GetBoolAnnotation(corsEnableAnnotation, ing, c.annotationConfig.Annotations)
if err != nil {
if errors.IsValidationError(err) {
klog.Warningf("enable-cors is invalid, defaulting to 'false'")
}
config.CorsEnabled = false
}
config.CorsAllowOrigin = []string{}
unparsedOrigins, err := parser.GetStringAnnotation("cors-allow-origin", ing)
unparsedOrigins, err := parser.GetStringAnnotation(corsAllowOriginAnnotation, ing, c.annotationConfig.Annotations)
if err == nil {
origins := strings.Split(unparsedOrigins, ",")
for _, origin := range origins {
@ -140,33 +214,53 @@ func (c cors) Parse(ing *networking.Ingress) (interface{}, error) {
klog.Infof("Current config.corsAllowOrigin %v", config.CorsAllowOrigin)
}
} else {
if errors.IsValidationError(err) {
klog.Warningf("cors-allow-origin is invalid, defaulting to '*'")
}
config.CorsAllowOrigin = []string{"*"}
}
config.CorsAllowHeaders, err = parser.GetStringAnnotation("cors-allow-headers", ing)
if err != nil || !corsHeadersRegex.MatchString(config.CorsAllowHeaders) {
config.CorsAllowHeaders, err = parser.GetStringAnnotation(corsAllowHeadersAnnotation, ing, c.annotationConfig.Annotations)
if err != nil || !parser.HeadersVariable.MatchString(config.CorsAllowHeaders) {
config.CorsAllowHeaders = defaultCorsHeaders
}
config.CorsAllowMethods, err = parser.GetStringAnnotation("cors-allow-methods", ing)
config.CorsAllowMethods, err = parser.GetStringAnnotation(corsAllowMethodsAnnotation, ing, c.annotationConfig.Annotations)
if err != nil || !corsMethodsRegex.MatchString(config.CorsAllowMethods) {
config.CorsAllowMethods = defaultCorsMethods
}
config.CorsAllowCredentials, err = parser.GetBoolAnnotation("cors-allow-credentials", ing)
config.CorsAllowCredentials, err = parser.GetBoolAnnotation(corsAllowCredentialsAnnotation, ing, c.annotationConfig.Annotations)
if err != nil {
if errors.IsValidationError(err) {
if errors.IsValidationError(err) {
klog.Warningf("cors-allow-credentials is invalid, defaulting to 'true'")
}
}
config.CorsAllowCredentials = true
}
config.CorsExposeHeaders, err = parser.GetStringAnnotation("cors-expose-headers", ing)
config.CorsExposeHeaders, err = parser.GetStringAnnotation(corsExposeHeadersAnnotation, ing, c.annotationConfig.Annotations)
if err != nil || !corsExposeHeadersRegex.MatchString(config.CorsExposeHeaders) {
config.CorsExposeHeaders = ""
}
config.CorsMaxAge, err = parser.GetIntAnnotation("cors-max-age", ing)
config.CorsMaxAge, err = parser.GetIntAnnotation(corsMaxAgeAnnotation, ing, c.annotationConfig.Annotations)
if err != nil {
if errors.IsValidationError(err) {
klog.Warningf("cors-max-age is invalid, defaulting to %d", defaultCorsMaxAge)
}
config.CorsMaxAge = defaultCorsMaxAge
}
return config, nil
}
func (c cors) GetDocumentation() parser.AnnotationFields {
return c.annotationConfig.Annotations
}
func (a cors) Validate(anns map[string]string) error {
maxrisk := parser.StringRiskToRisk(a.r.GetSecurityConfiguration().AnnotationsRiskLevel)
return parser.CheckAnnotationRisk(anns, maxrisk, corsAnnotation.Annotations)
}

View file

@ -75,13 +75,13 @@ func TestIngressCorsConfigValid(t *testing.T) {
data := map[string]string{}
// Valid
data[parser.GetAnnotationWithPrefix("enable-cors")] = "true"
data[parser.GetAnnotationWithPrefix("cors-allow-headers")] = "DNT,X-CustomHeader, Keep-Alive,User-Agent"
data[parser.GetAnnotationWithPrefix("cors-allow-credentials")] = "false"
data[parser.GetAnnotationWithPrefix("cors-allow-methods")] = "GET, PATCH"
data[parser.GetAnnotationWithPrefix("cors-allow-origin")] = "https://origin123.test.com:4443"
data[parser.GetAnnotationWithPrefix("cors-expose-headers")] = "*, X-CustomResponseHeader"
data[parser.GetAnnotationWithPrefix("cors-max-age")] = "600"
data[parser.GetAnnotationWithPrefix(corsEnableAnnotation)] = "true"
data[parser.GetAnnotationWithPrefix(corsAllowHeadersAnnotation)] = "DNT,X-CustomHeader, Keep-Alive,User-Agent"
data[parser.GetAnnotationWithPrefix(corsAllowCredentialsAnnotation)] = "false"
data[parser.GetAnnotationWithPrefix(corsAllowMethodsAnnotation)] = "GET, PATCH"
data[parser.GetAnnotationWithPrefix(corsAllowOriginAnnotation)] = "https://origin123.test.com:4443"
data[parser.GetAnnotationWithPrefix(corsExposeHeadersAnnotation)] = "*, X-CustomResponseHeader"
data[parser.GetAnnotationWithPrefix(corsMaxAgeAnnotation)] = "600"
ing.SetAnnotations(data)
corst, err := NewParser(&resolver.Mock{}).Parse(ing)
@ -95,31 +95,31 @@ func TestIngressCorsConfigValid(t *testing.T) {
}
if !nginxCors.CorsEnabled {
t.Errorf("expected %v but returned %v", data[parser.GetAnnotationWithPrefix("enable-cors")], nginxCors.CorsEnabled)
t.Errorf("expected %v but returned %v", data[parser.GetAnnotationWithPrefix(corsEnableAnnotation)], nginxCors.CorsEnabled)
}
if nginxCors.CorsAllowCredentials {
t.Errorf("expected %v but returned %v", data[parser.GetAnnotationWithPrefix("cors-allow-credentials")], nginxCors.CorsAllowCredentials)
t.Errorf("expected %v but returned %v", data[parser.GetAnnotationWithPrefix(corsAllowCredentialsAnnotation)], nginxCors.CorsAllowCredentials)
}
if nginxCors.CorsAllowHeaders != "DNT,X-CustomHeader, Keep-Alive,User-Agent" {
t.Errorf("expected %v but returned %v", data[parser.GetAnnotationWithPrefix("cors-allow-headers")], nginxCors.CorsAllowHeaders)
t.Errorf("expected %v but returned %v", data[parser.GetAnnotationWithPrefix(corsAllowHeadersAnnotation)], nginxCors.CorsAllowHeaders)
}
if nginxCors.CorsAllowMethods != "GET, PATCH" {
t.Errorf("expected %v but returned %v", data[parser.GetAnnotationWithPrefix("cors-allow-methods")], nginxCors.CorsAllowMethods)
t.Errorf("expected %v but returned %v", data[parser.GetAnnotationWithPrefix(corsAllowMethodsAnnotation)], nginxCors.CorsAllowMethods)
}
if nginxCors.CorsAllowOrigin[0] != "https://origin123.test.com:4443" {
t.Errorf("expected %v but returned %v", data[parser.GetAnnotationWithPrefix("cors-allow-origin")], nginxCors.CorsAllowOrigin)
t.Errorf("expected %v but returned %v", data[parser.GetAnnotationWithPrefix(corsAllowOriginAnnotation)], nginxCors.CorsAllowOrigin)
}
if nginxCors.CorsExposeHeaders != "*, X-CustomResponseHeader" {
t.Errorf("expected %v but returned %v", data[parser.GetAnnotationWithPrefix("cors-expose-headers")], nginxCors.CorsExposeHeaders)
t.Errorf("expected %v but returned %v", data[parser.GetAnnotationWithPrefix(corsExposeHeadersAnnotation)], nginxCors.CorsExposeHeaders)
}
if nginxCors.CorsMaxAge != 600 {
t.Errorf("expected %v but returned %v", data[parser.GetAnnotationWithPrefix("cors-max-age")], nginxCors.CorsMaxAge)
t.Errorf("expected %v but returned %v", data[parser.GetAnnotationWithPrefix(corsMaxAgeAnnotation)], nginxCors.CorsMaxAge)
}
}
@ -129,13 +129,13 @@ func TestIngressCorsConfigInvalid(t *testing.T) {
data := map[string]string{}
// Valid
data[parser.GetAnnotationWithPrefix("enable-cors")] = "yes"
data[parser.GetAnnotationWithPrefix("cors-allow-headers")] = "@alright, #ingress"
data[parser.GetAnnotationWithPrefix("cors-allow-credentials")] = "no"
data[parser.GetAnnotationWithPrefix("cors-allow-methods")] = "GET, PATCH, $nginx"
data[parser.GetAnnotationWithPrefix("cors-allow-origin")] = "origin123.test.com:4443"
data[parser.GetAnnotationWithPrefix("cors-expose-headers")] = "@alright, #ingress"
data[parser.GetAnnotationWithPrefix("cors-max-age")] = "abcd"
data[parser.GetAnnotationWithPrefix(corsEnableAnnotation)] = "yes"
data[parser.GetAnnotationWithPrefix(corsAllowHeadersAnnotation)] = "@alright, #ingress"
data[parser.GetAnnotationWithPrefix(corsAllowCredentialsAnnotation)] = "no"
data[parser.GetAnnotationWithPrefix(corsAllowMethodsAnnotation)] = "GET, PATCH, $nginx"
data[parser.GetAnnotationWithPrefix(corsAllowOriginAnnotation)] = "origin123.test.com:4443"
data[parser.GetAnnotationWithPrefix(corsExposeHeadersAnnotation)] = "@alright, #ingress"
data[parser.GetAnnotationWithPrefix(corsMaxAgeAnnotation)] = "abcd"
ing.SetAnnotations(data)
corst, err := NewParser(&resolver.Mock{}).Parse(ing)

View file

@ -17,6 +17,7 @@ limitations under the License.
package customhttperrors
import (
"regexp"
"strconv"
"strings"
@ -26,19 +27,46 @@ import (
"k8s.io/ingress-nginx/internal/ingress/resolver"
)
const (
customHTTPErrorsAnnotation = "custom-http-errors"
)
var (
// We accept anything between 400 and 599, on a comma separated.
arrayOfHTTPErrors = regexp.MustCompile(`^(?:[4,5][0-9][0-9],?)*$`)
)
var customHTTPErrorsAnnotations = parser.Annotation{
Group: "backend",
Annotations: parser.AnnotationFields{
customHTTPErrorsAnnotation: {
Validator: parser.ValidateRegex(*arrayOfHTTPErrors, true),
Scope: parser.AnnotationScopeLocation,
Risk: parser.AnnotationRiskLow,
Documentation: `If a default backend annotation is specified on the ingress, the errors code specified on this annotation
will be routed to that annotation's default backend service. Otherwise they will be routed to the global default backend.
A comma-separated list of error codes is accepted (anything between 400 and 599, like 403, 503)`,
},
},
}
type customhttperrors struct {
r resolver.Resolver
r resolver.Resolver
annotationConfig parser.Annotation
}
// NewParser creates a new custom http errors annotation parser
func NewParser(r resolver.Resolver) parser.IngressAnnotation {
return customhttperrors{r}
return customhttperrors{
r: r,
annotationConfig: customHTTPErrorsAnnotations,
}
}
// Parse parses the annotations contained in the ingress to use
// custom http errors
func (e customhttperrors) Parse(ing *networking.Ingress) (interface{}, error) {
c, err := parser.GetStringAnnotation("custom-http-errors", ing)
c, err := parser.GetStringAnnotation(customHTTPErrorsAnnotation, ing, e.annotationConfig.Annotations)
if err != nil {
return nil, err
}
@ -55,3 +83,12 @@ func (e customhttperrors) Parse(ing *networking.Ingress) (interface{}, error) {
return codes, nil
}
func (e customhttperrors) GetDocumentation() parser.AnnotationFields {
return e.annotationConfig.Annotations
}
func (a customhttperrors) Validate(anns map[string]string) error {
maxrisk := parser.StringRiskToRisk(a.r.GetSecurityConfiguration().AnnotationsRiskLevel)
return parser.CheckAnnotationRisk(anns, maxrisk, customHTTPErrorsAnnotations.Annotations)
}

View file

@ -25,19 +25,40 @@ import (
"k8s.io/ingress-nginx/internal/ingress/resolver"
)
const (
defaultBackendAnnotation = "default-backend"
)
var defaultBackendAnnotations = parser.Annotation{
Group: "backend",
Annotations: parser.AnnotationFields{
defaultBackendAnnotation: {
Validator: parser.ValidateServiceName,
Scope: parser.AnnotationScopeLocation,
Risk: parser.AnnotationRiskLow,
Documentation: `This service will be used to handle the response when the configured service in the Ingress rule does not have any active endpoints.
It will also be used to handle the error responses if both this annotation and the custom-http-errors annotation are set.`,
},
},
}
type backend struct {
r resolver.Resolver
r resolver.Resolver
annotationConfig parser.Annotation
}
// NewParser creates a new default backend annotation parser
func NewParser(r resolver.Resolver) parser.IngressAnnotation {
return backend{r}
return backend{
r: r,
annotationConfig: defaultBackendAnnotations,
}
}
// Parse parses the annotations contained in the ingress to use
// a custom default backend
func (db backend) Parse(ing *networking.Ingress) (interface{}, error) {
s, err := parser.GetStringAnnotation("default-backend", ing)
s, err := parser.GetStringAnnotation(defaultBackendAnnotation, ing, db.annotationConfig.Annotations)
if err != nil {
return nil, err
}
@ -50,3 +71,12 @@ func (db backend) Parse(ing *networking.Ingress) (interface{}, error) {
return svc, nil
}
func (db backend) GetDocumentation() parser.AnnotationFields {
return db.annotationConfig.Annotations
}
func (a backend) Validate(anns map[string]string) error {
maxrisk := parser.StringRiskToRisk(a.r.GetSecurityConfiguration().AnnotationsRiskLevel)
return parser.CheckAnnotationRisk(anns, maxrisk, defaultBackendAnnotations.Annotations)
}

View file

@ -91,21 +91,51 @@ func (m mockService) GetService(name string) (*api.Service, error) {
func TestAnnotations(t *testing.T) {
ing := buildIngress()
data := map[string]string{}
data[parser.GetAnnotationWithPrefix("default-backend")] = "demo-service"
ing.SetAnnotations(data)
fakeService := &mockService{}
i, err := NewParser(fakeService).Parse(ing)
if err != nil {
t.Errorf("unexpected error %v", err)
tests := map[string]struct {
expectErr bool
serviceName string
}{
"valid name": {
serviceName: "demo-service",
expectErr: false,
},
"not in backend": {
serviceName: "demo1-service",
expectErr: true,
},
"invalid dns name": {
serviceName: "demo-service.something.tld",
expectErr: true,
},
"invalid name": {
serviceName: "something/xpto",
expectErr: true,
},
"invalid characters": {
serviceName: "something;xpto",
expectErr: true,
},
}
svc, ok := i.(*api.Service)
if !ok {
t.Errorf("expected *api.Service but got %v", svc)
}
if svc.Name != "demo-service" {
t.Errorf("expected %v but got %v", "demo-service", svc.Name)
for _, test := range tests {
data := map[string]string{}
data[parser.GetAnnotationWithPrefix(defaultBackendAnnotation)] = test.serviceName
ing.SetAnnotations(data)
fakeService := &mockService{}
i, err := NewParser(fakeService).Parse(ing)
if (err != nil) != test.expectErr {
t.Errorf("expected error: %t got error: %t err value: %s. %+v", test.expectErr, err != nil, err, i)
}
if !test.expectErr {
svc, ok := i.(*api.Service)
if !ok {
t.Errorf("expected *api.Service but got %v", svc)
}
if svc.Name != test.serviceName {
t.Errorf("expected %v but got %v", test.serviceName, svc.Name)
}
}
}
}

View file

@ -19,17 +19,49 @@ package fastcgi
import (
"fmt"
"reflect"
"regexp"
networking "k8s.io/api/networking/v1"
"k8s.io/client-go/tools/cache"
"k8s.io/klog/v2"
"k8s.io/ingress-nginx/internal/ingress/annotations/parser"
ing_errors "k8s.io/ingress-nginx/internal/ingress/errors"
"k8s.io/ingress-nginx/internal/ingress/resolver"
)
const (
fastCGIIndexAnnotation = "fastcgi-index"
fastCGIParamsAnnotation = "fastcgi-params-configmap"
)
var (
// fast-cgi valid parameters is just a single file name (like index.php)
regexValidIndexAnnotationAndKey = regexp.MustCompile(`^[A-Za-z0-9\.\-\_]+$`)
)
var fastCGIAnnotations = parser.Annotation{
Group: "fastcgi",
Annotations: parser.AnnotationFields{
fastCGIIndexAnnotation: {
Validator: parser.ValidateRegex(*regexValidIndexAnnotationAndKey, true),
Scope: parser.AnnotationScopeLocation,
Risk: parser.AnnotationRiskMedium,
Documentation: `This annotation can be used to specify an index file`,
},
fastCGIParamsAnnotation: {
Validator: parser.ValidateRegex(*parser.BasicCharsRegex, true),
Scope: parser.AnnotationScopeLocation,
Risk: parser.AnnotationRiskMedium,
Documentation: `This annotation can be used to specify a ConfigMap containing the fastcgi parameters as a key/value.
Only ConfigMaps on the same namespace of ingress can be used. They key and value from ConfigMap are validated for unauthorized characters.`,
},
},
}
type fastcgi struct {
r resolver.Resolver
r resolver.Resolver
annotationConfig parser.Annotation
}
// Config describes the per location fastcgi config
@ -57,7 +89,10 @@ func (l1 *Config) Equal(l2 *Config) bool {
// NewParser creates a new fastcgiConfig protocol annotation parser
func NewParser(r resolver.Resolver) parser.IngressAnnotation {
return fastcgi{r}
return fastcgi{
r: r,
annotationConfig: fastCGIAnnotations,
}
}
// ParseAnnotations parses the annotations contained in the ingress
@ -70,14 +105,21 @@ func (a fastcgi) Parse(ing *networking.Ingress) (interface{}, error) {
return fcgiConfig, nil
}
index, err := parser.GetStringAnnotation("fastcgi-index", ing)
index, err := parser.GetStringAnnotation(fastCGIIndexAnnotation, ing, a.annotationConfig.Annotations)
if err != nil {
if ing_errors.IsValidationError(err) {
return fcgiConfig, err
}
index = ""
}
fcgiConfig.Index = index
cm, err := parser.GetStringAnnotation("fastcgi-params-configmap", ing)
cm, err := parser.GetStringAnnotation(fastCGIParamsAnnotation, ing, a.annotationConfig.Annotations)
if err != nil {
if ing_errors.IsValidationError(err) {
return fcgiConfig, err
}
return fcgiConfig, nil
}
@ -87,8 +129,10 @@ func (a fastcgi) Parse(ing *networking.Ingress) (interface{}, error) {
Reason: fmt.Errorf("error reading configmap name from annotation: %w", err),
}
}
secCfg := a.r.GetSecurityConfiguration()
if cmns != "" && cmns != ing.Namespace {
// We don't accept different namespaces for secrets.
if cmns != "" && !secCfg.AllowCrossNamespaceResources && cmns != ing.Namespace {
return fcgiConfig, fmt.Errorf("different namespace is not supported on fast_cgi param configmap")
}
@ -100,7 +144,24 @@ func (a fastcgi) Parse(ing *networking.Ingress) (interface{}, error) {
}
}
for k, v := range cmap.Data {
if !regexValidIndexAnnotationAndKey.MatchString(k) || !parser.NGINXVariable.MatchString(v) {
klog.ErrorS(fmt.Errorf("fcgi contains invalid key or value"), "fcgi annotation error", "configmap", cmap.Name, "namespace", cmap.Namespace, "key", k, "value", v)
return fcgiConfig, ing_errors.NewValidationError(fastCGIParamsAnnotation)
}
}
fcgiConfig.Index = index
fcgiConfig.Params = cmap.Data
return fcgiConfig, nil
}
func (a fastcgi) GetDocumentation() parser.AnnotationFields {
return a.annotationConfig.Annotations
}
func (a fastcgi) Validate(anns map[string]string) error {
maxrisk := parser.StringRiskToRisk(a.r.GetSecurityConfiguration().AnnotationsRiskLevel)
return parser.CheckAnnotationRisk(anns, maxrisk, fastCGIAnnotations.Annotations)
}

View file

@ -18,6 +18,7 @@ package fastcgi
import (
"fmt"
"reflect"
"testing"
api "k8s.io/api/core/v1"
@ -49,10 +50,16 @@ func buildIngress() *networking.Ingress {
type mockConfigMap struct {
resolver.Mock
extraConfigMap map[string]map[string]string
}
func (m mockConfigMap) GetConfigMap(name string) (*api.ConfigMap, error) {
if name != "default/demo-configmap" && name != "otherns/demo-configmap" {
if m.extraConfigMap == nil {
m.extraConfigMap = make(map[string]map[string]string)
}
cmdata, ok := m.extraConfigMap[name]
if name != "default/demo-configmap" && name != "otherns/demo-configmap" && !ok {
return nil, fmt.Errorf("there is no configmap with name %v", name)
}
@ -61,12 +68,17 @@ func (m mockConfigMap) GetConfigMap(name string) (*api.ConfigMap, error) {
return nil, fmt.Errorf("invalid configmap name")
}
data := map[string]string{"REDIRECT_STATUS": "200", "SERVER_NAME": "$server_name"}
if ok {
data = cmdata
}
return &api.ConfigMap{
ObjectMeta: meta_v1.ObjectMeta{
Namespace: cmns,
Name: cmn,
},
Data: map[string]string{"REDIRECT_STATUS": "200", "SERVER_NAME": "$server_name"},
Data: data,
}, nil
}
@ -283,3 +295,111 @@ func TestConfigEquality(t *testing.T) {
t.Errorf("config4 should be equal to config")
}
}
func Test_fastcgi_Parse(t *testing.T) {
tests := []struct {
name string
index string
configmapname string
configmap map[string]string
want interface{}
wantErr bool
}{
{
name: "valid configuration",
index: "indexxpto-92123.php",
configmapname: "default/fcgiconfig",
configmap: map[string]string{
"REQUEST_METHOD": "$request_method",
"SCRIPT_FILENAME": "$document_root$fastcgi_script_name",
},
want: Config{
Index: "indexxpto-92123.php",
Params: map[string]string{
"REQUEST_METHOD": "$request_method",
"SCRIPT_FILENAME": "$document_root$fastcgi_script_name",
},
},
},
{
name: "invalid index name",
index: "indexxpto-92123$xx.php",
configmapname: "default/fcgiconfig",
configmap: map[string]string{
"REQUEST_METHOD": "$request_method",
"SCRIPT_FILENAME": "$document_root$fastcgi_script_name",
},
want: Config{},
wantErr: true,
},
{
name: "invalid configmap namespace",
index: "indexxpto-92123.php",
configmapname: "otherns/fcgiconfig",
configmap: map[string]string{
"REQUEST_METHOD": "$request_method",
"SCRIPT_FILENAME": "$document_root$fastcgi_script_name",
},
want: Config{Index: "indexxpto-92123.php"},
wantErr: true,
},
{
name: "invalid configmap namespace name",
index: "indexxpto-92123.php",
configmapname: "otherns/fcgicon;{fig",
configmap: map[string]string{
"REQUEST_METHOD": "$request_method",
"SCRIPT_FILENAME": "$document_root$fastcgi_script_name",
},
want: Config{Index: "indexxpto-92123.php"},
wantErr: true,
},
{
name: "invalid configmap values key",
index: "indexxpto-92123.php",
configmapname: "default/fcgiconfig",
configmap: map[string]string{
"REQUEST_METHOD$XPTO": "$request_method",
},
want: Config{Index: "indexxpto-92123.php"},
wantErr: true,
},
{
name: "invalid configmap values val",
index: "indexxpto-92123.php",
configmapname: "default/fcgiconfig",
configmap: map[string]string{
"REQUEST_METHOD_XPTO": "$request_method{test};a",
},
want: Config{Index: "indexxpto-92123.php"},
wantErr: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
ing := buildIngress()
data := map[string]string{}
data[parser.GetAnnotationWithPrefix("fastcgi-index")] = tt.index
data[parser.GetAnnotationWithPrefix("fastcgi-params-configmap")] = tt.configmapname
ing.SetAnnotations(data)
m := &mockConfigMap{
extraConfigMap: map[string]map[string]string{
tt.configmapname: tt.configmap,
},
}
got, err := NewParser(m).Parse(ing)
if (err != nil) != tt.wantErr {
t.Errorf("fastcgi.Parse() error = %v, wantErr %v", err, tt.wantErr)
return
}
if !reflect.DeepEqual(got, tt.want) {
t.Errorf("fastcgi.Parse() = %v, want %v", got, tt.want)
}
})
}
}

View file

@ -22,8 +22,10 @@ import (
"time"
networking "k8s.io/api/networking/v1"
"k8s.io/klog/v2"
"k8s.io/ingress-nginx/internal/ingress/annotations/parser"
"k8s.io/ingress-nginx/internal/ingress/errors"
ing_errors "k8s.io/ingress-nginx/internal/ingress/errors"
"k8s.io/ingress-nginx/internal/ingress/resolver"
"k8s.io/ingress-nginx/internal/net"
@ -32,6 +34,46 @@ import (
const defaultKey = "$remote_addr"
const (
globalRateLimitAnnotation = "global-rate-limit"
globalRateLimitWindowAnnotation = "global-rate-limit-window"
globalRateLimitKeyAnnotation = "global-rate-limit-key"
globalRateLimitIgnoredCidrsAnnotation = "global-rate-limit-ignored-cidrs"
)
var globalRateLimitAnnotationConfig = parser.Annotation{
Group: "ratelimit",
Annotations: parser.AnnotationFields{
globalRateLimitAnnotation: {
Validator: parser.ValidateInt,
Scope: parser.AnnotationScopeIngress,
Risk: parser.AnnotationRiskLow,
Documentation: `This annotation configures maximum allowed number of requests per window`,
},
globalRateLimitWindowAnnotation: {
Validator: parser.ValidateDuration,
Scope: parser.AnnotationScopeIngress,
Risk: parser.AnnotationRiskLow,
Documentation: `Configures a time window (i.e 1m) that the limit is applied`,
},
globalRateLimitKeyAnnotation: {
Validator: parser.ValidateRegex(*parser.NGINXVariable, true),
Scope: parser.AnnotationScopeIngress,
Risk: parser.AnnotationRiskHigh,
Documentation: `This annotation Configures a key for counting the samples. Defaults to $remote_addr.
You can also combine multiple NGINX variables here, like ${remote_addr}-${http_x_api_client} which would mean the limit will be applied to
requests coming from the same API client (indicated by X-API-Client HTTP request header) with the same source IP address`,
},
globalRateLimitIgnoredCidrsAnnotation: {
Validator: parser.ValidateCIDRs,
Scope: parser.AnnotationScopeIngress,
Risk: parser.AnnotationRiskMedium,
Documentation: `This annotation defines a comma separated list of IPs and CIDRs to match client IP against.
When there's a match request is not considered for rate limiting.`,
},
},
}
// Config encapsulates all global rate limit attributes
type Config struct {
Namespace string `json:"namespace"`
@ -63,12 +105,16 @@ func (l *Config) Equal(r *Config) bool {
}
type globalratelimit struct {
r resolver.Resolver
r resolver.Resolver
annotationConfig parser.Annotation
}
// NewParser creates a new globalratelimit annotation parser
func NewParser(r resolver.Resolver) parser.IngressAnnotation {
return globalratelimit{r}
return globalratelimit{
r: r,
annotationConfig: globalRateLimitAnnotationConfig,
}
}
// Parse extracts globalratelimit annotations from the given ingress
@ -76,8 +122,16 @@ func NewParser(r resolver.Resolver) parser.IngressAnnotation {
func (a globalratelimit) Parse(ing *networking.Ingress) (interface{}, error) {
config := &Config{}
limit, _ := parser.GetIntAnnotation("global-rate-limit", ing)
rawWindowSize, _ := parser.GetStringAnnotation("global-rate-limit-window", ing)
limit, err := parser.GetIntAnnotation(globalRateLimitAnnotation, ing, a.annotationConfig.Annotations)
if err != nil && errors.IsInvalidContent(err) {
return nil, err
}
rawWindowSize, err := parser.GetStringAnnotation(globalRateLimitWindowAnnotation, ing, a.annotationConfig.Annotations)
if err != nil && errors.IsValidationError(err) {
return config, ing_errors.LocationDenied{
Reason: fmt.Errorf("failed to parse 'global-rate-limit-window' value: %w", err),
}
}
if limit == 0 || len(rawWindowSize) == 0 {
return config, nil
@ -90,12 +144,18 @@ func (a globalratelimit) Parse(ing *networking.Ingress) (interface{}, error) {
}
}
key, _ := parser.GetStringAnnotation("global-rate-limit-key", ing)
key, err := parser.GetStringAnnotation(globalRateLimitKeyAnnotation, ing, a.annotationConfig.Annotations)
if err != nil {
klog.Warningf("invalid %s, defaulting to %s", globalRateLimitKeyAnnotation, defaultKey)
}
if len(key) == 0 {
key = defaultKey
}
rawIgnoredCIDRs, _ := parser.GetStringAnnotation("global-rate-limit-ignored-cidrs", ing)
rawIgnoredCIDRs, err := parser.GetStringAnnotation(globalRateLimitIgnoredCidrsAnnotation, ing, a.annotationConfig.Annotations)
if err != nil && errors.IsInvalidContent(err) {
return nil, err
}
ignoredCIDRs, err := net.ParseCIDRs(rawIgnoredCIDRs)
if err != nil {
return nil, err
@ -109,3 +169,12 @@ func (a globalratelimit) Parse(ing *networking.Ingress) (interface{}, error) {
return config, nil
}
func (a globalratelimit) GetDocumentation() parser.AnnotationFields {
return a.annotationConfig.Annotations
}
func (a globalratelimit) Validate(anns map[string]string) error {
maxrisk := parser.StringRiskToRisk(a.r.GetSecurityConfiguration().AnnotationsRiskLevel)
return parser.CheckAnnotationRisk(anns, maxrisk, globalRateLimitAnnotationConfig.Annotations)
}

View file

@ -149,6 +149,22 @@ func TestGlobalRateLimiting(t *testing.T) {
},
nil,
},
{
"global-rate-limit-complex-key",
map[string]string{
annRateLimit: "100",
annRateLimitWindow: "2m",
annRateLimitKey: "${http_x_api_user}${otherinfo}",
},
&Config{
Namespace: expectedUID,
Limit: 100,
WindowSize: 120,
Key: "${http_x_api_user}${otherinfo}",
IgnoredCIDRs: make([]string, 0),
},
nil,
},
{
"incorrect duration for window",
map[string]string{
@ -157,8 +173,8 @@ func TestGlobalRateLimiting(t *testing.T) {
annRateLimitKey: "$http_x_api_user",
},
&Config{},
ing_errors.LocationDenied{
Reason: fmt.Errorf("failed to parse 'global-rate-limit-window' value: time: unknown unit \"mb\" in duration \"2mb\""),
ing_errors.ValidationError{
Reason: fmt.Errorf("failed to parse 'global-rate-limit-window' value: annotation nginx.ingress.kubernetes.io/global-rate-limit-window contains invalid value"),
},
},
}
@ -168,7 +184,7 @@ func TestGlobalRateLimiting(t *testing.T) {
i, actualErr := NewParser(mockBackend{}).Parse(ing)
if (testCase.expectedErr == nil || actualErr == nil) && testCase.expectedErr != actualErr {
t.Errorf("expected error 'nil' but got '%v'", actualErr)
t.Errorf("%s expected error '%v' but got '%v'", testCase.title, testCase.expectedErr, actualErr)
} else if testCase.expectedErr != nil && actualErr != nil &&
testCase.expectedErr.Error() != actualErr.Error() {
t.Errorf("expected error '%v' but got '%v'", testCase.expectedErr, actualErr)

View file

@ -23,17 +23,46 @@ import (
"k8s.io/ingress-nginx/internal/ingress/resolver"
)
const (
http2PushPreloadAnnotation = "http2-push-preload"
)
var http2PushPreloadAnnotations = parser.Annotation{
Group: "http2",
Annotations: parser.AnnotationFields{
http2PushPreloadAnnotation: {
Validator: parser.ValidateBool,
Scope: parser.AnnotationScopeLocation,
Risk: parser.AnnotationRiskLow,
Documentation: `Enables automatic conversion of preload links specified in the “Link” response header fields into push requests`,
},
},
}
type http2PushPreload struct {
r resolver.Resolver
r resolver.Resolver
annotationConfig parser.Annotation
}
// NewParser creates a new http2PushPreload annotation parser
func NewParser(r resolver.Resolver) parser.IngressAnnotation {
return http2PushPreload{r}
return http2PushPreload{
r: r,
annotationConfig: http2PushPreloadAnnotations,
}
}
// Parse parses the annotations contained in the ingress rule
// used to add http2 push preload to the server
func (h2pp http2PushPreload) Parse(ing *networking.Ingress) (interface{}, error) {
return parser.GetBoolAnnotation("http2-push-preload", ing)
return parser.GetBoolAnnotation(http2PushPreloadAnnotation, ing, h2pp.annotationConfig.Annotations)
}
func (h2pp http2PushPreload) GetDocumentation() parser.AnnotationFields {
return h2pp.annotationConfig.Annotations
}
func (a http2PushPreload) Validate(anns map[string]string) error {
maxrisk := parser.StringRiskToRisk(a.r.GetSecurityConfiguration().AnnotationsRiskLevel)
return parser.CheckAnnotationRisk(anns, maxrisk, http2PushPreloadAnnotations.Annotations)
}

View file

@ -23,11 +23,12 @@ import (
networking "k8s.io/api/networking/v1"
meta_v1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/ingress-nginx/internal/ingress/annotations/parser"
"k8s.io/ingress-nginx/internal/ingress/errors"
"k8s.io/ingress-nginx/internal/ingress/resolver"
)
func TestParse(t *testing.T) {
annotation := parser.GetAnnotationWithPrefix("http2-push-preload")
annotation := parser.GetAnnotationWithPrefix(http2PushPreloadAnnotation)
ap := NewParser(&resolver.Mock{})
if ap == nil {
t.Fatalf("expected a parser.IngressAnnotation but returned nil")
@ -36,12 +37,14 @@ func TestParse(t *testing.T) {
testCases := []struct {
annotations map[string]string
expected bool
expectErr bool
}{
{map[string]string{annotation: "true"}, true},
{map[string]string{annotation: "1"}, true},
{map[string]string{annotation: ""}, false},
{map[string]string{}, false},
{nil, false},
{map[string]string{annotation: "true"}, true, false},
{map[string]string{annotation: "1"}, true, false},
{map[string]string{annotation: "xpto"}, false, true},
{map[string]string{annotation: ""}, false, false},
{map[string]string{}, false, false},
{nil, false, false},
}
ing := &networking.Ingress{
@ -54,7 +57,10 @@ func TestParse(t *testing.T) {
for _, testCase := range testCases {
ing.SetAnnotations(testCase.annotations)
result, _ := ap.Parse(ing)
result, err := ap.Parse(ing)
if ((err != nil) != testCase.expectErr) && !errors.IsInvalidContent(err) && !errors.IsMissingAnnotations(err) {
t.Fatalf("expected error: %t got error: %t err value: %s. %+v", testCase.expectErr, err != nil, err, testCase.annotations)
}
if result != testCase.expected {
t.Errorf("expected %v but returned %v, annotations: %s", testCase.expected, result, testCase.annotations)
}

View file

@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
package ipwhitelist
package ipallowlist
import (
"fmt"
@ -30,6 +30,24 @@ import (
"k8s.io/ingress-nginx/pkg/util/sets"
)
const (
ipWhitelistAnnotation = "whitelist-source-range"
ipAllowlistAnnotation = "allowlist-source-range"
)
var allowlistAnnotations = parser.Annotation{
Group: "acl",
Annotations: parser.AnnotationFields{
ipAllowlistAnnotation: {
Validator: parser.ValidateCIDRs,
Scope: parser.AnnotationScopeLocation,
Risk: parser.AnnotationRiskMedium, // Failure on parsing this may cause undesired access
Documentation: `This annotation allows setting a list of IPs and networks allowed to access this Location`,
AnnotationAliases: []string{ipWhitelistAnnotation},
},
},
}
// SourceRange returns the CIDR
type SourceRange struct {
CIDR []string `json:"cidr,omitempty"`
@ -47,36 +65,47 @@ func (sr1 *SourceRange) Equal(sr2 *SourceRange) bool {
return sets.StringElementsMatch(sr1.CIDR, sr2.CIDR)
}
type ipwhitelist struct {
r resolver.Resolver
type ipallowlist struct {
r resolver.Resolver
annotationConfig parser.Annotation
}
// NewParser creates a new whitelist annotation parser
// NewParser creates a new ipallowlist annotation parser
func NewParser(r resolver.Resolver) parser.IngressAnnotation {
return ipwhitelist{r}
return ipallowlist{
r: r,
annotationConfig: allowlistAnnotations,
}
}
// ParseAnnotations parses the annotations contained in the ingress
// rule used to limit access to certain client addresses or networks.
// Multiple ranges can specified using commas as separator
// e.g. `18.0.0.0/8,56.0.0.0/8`
func (a ipwhitelist) Parse(ing *networking.Ingress) (interface{}, error) {
func (a ipallowlist) Parse(ing *networking.Ingress) (interface{}, error) {
defBackend := a.r.GetDefaultBackend()
defaultWhitelistSourceRange := make([]string, len(defBackend.WhitelistSourceRange))
copy(defaultWhitelistSourceRange, defBackend.WhitelistSourceRange)
sort.Strings(defaultWhitelistSourceRange)
defaultAllowlistSourceRange := make([]string, len(defBackend.WhitelistSourceRange))
copy(defaultAllowlistSourceRange, defBackend.WhitelistSourceRange)
sort.Strings(defaultAllowlistSourceRange)
val, err := parser.GetStringAnnotation("whitelist-source-range", ing)
val, err := parser.GetStringAnnotation(ipAllowlistAnnotation, ing, a.annotationConfig.Annotations)
// A missing annotation is not a problem, just use the default
if err == ing_errors.ErrMissingAnnotations {
return &SourceRange{CIDR: defaultWhitelistSourceRange}, nil
if err != nil {
if err == ing_errors.ErrMissingAnnotations {
return &SourceRange{CIDR: defaultAllowlistSourceRange}, nil
}
return &SourceRange{CIDR: defaultAllowlistSourceRange}, ing_errors.LocationDenied{
Reason: err,
}
}
values := strings.Split(val, ",")
ipnets, ips, err := net.ParseIPNets(values...)
if err != nil && len(ips) == 0 {
return &SourceRange{CIDR: defaultWhitelistSourceRange}, ing_errors.LocationDenied{
return &SourceRange{CIDR: defaultAllowlistSourceRange}, ing_errors.LocationDenied{
Reason: fmt.Errorf("the annotation does not contain a valid IP address or network: %w", err),
}
}
@ -93,3 +122,12 @@ func (a ipwhitelist) Parse(ing *networking.Ingress) (interface{}, error) {
return &SourceRange{cidrs}, nil
}
func (a ipallowlist) GetDocumentation() parser.AnnotationFields {
return a.annotationConfig.Annotations
}
func (a ipallowlist) Validate(anns map[string]string) error {
maxrisk := parser.StringRiskToRisk(a.r.GetSecurityConfiguration().AnnotationsRiskLevel)
return parser.CheckAnnotationRisk(anns, maxrisk, allowlistAnnotations.Annotations)
}

View file

@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
package ipwhitelist
package ipallowlist
import (
"testing"
@ -86,12 +86,12 @@ func TestParseAnnotations(t *testing.T) {
"test parse a invalid net": {
net: "ww",
expectErr: true,
errOut: "the annotation does not contain a valid IP address or network: invalid CIDR address: ww",
errOut: "annotation nginx.ingress.kubernetes.io/allowlist-source-range contains invalid value",
},
"test parse a empty net": {
net: "",
expectErr: true,
errOut: "the annotation does not contain a valid IP address or network: invalid CIDR address: ",
errOut: "the annotation nginx.ingress.kubernetes.io/allowlist-source-range does not contain a valid value ()",
},
"test parse multiple valid cidr": {
net: "2.2.2.2/32,1.1.1.1/32,3.3.3.0/24",
@ -102,16 +102,16 @@ func TestParseAnnotations(t *testing.T) {
for testName, test := range tests {
data := map[string]string{}
data[parser.GetAnnotationWithPrefix("whitelist-source-range")] = test.net
data[parser.GetAnnotationWithPrefix(ipAllowlistAnnotation)] = test.net
ing.SetAnnotations(data)
p := NewParser(&resolver.Mock{})
i, err := p.Parse(ing)
if err != nil && !test.expectErr {
t.Errorf("%v:unexpected error: %v", testName, err)
if (err != nil) != test.expectErr {
t.Errorf("%s expected error: %t got error: %t err value: %s. %+v", testName, test.expectErr, err != nil, err, i)
}
if test.expectErr {
if test.expectErr && err != nil {
if err.Error() != test.errOut {
t.Errorf("%v:expected error: %v but %v return", testName, test.errOut, err.Error())
t.Errorf("expected error %s but got %s", test.errOut, err)
}
}
if !test.expectErr {
@ -137,7 +137,7 @@ func (m mockBackend) GetDefaultBackend() defaults.Backend {
}
}
// Test that when we have a whitelist set on the Backend that is used when we
// Test that when we have a allowlist set on the Backend that is used when we
// don't have the annotation
func TestParseAnnotationsWithDefaultConfig(t *testing.T) {
ing := buildIngress()
@ -158,12 +158,12 @@ func TestParseAnnotationsWithDefaultConfig(t *testing.T) {
"test parse a invalid net": {
net: "ww",
expectErr: true,
errOut: "the annotation does not contain a valid IP address or network: invalid CIDR address: ww",
errOut: "annotation nginx.ingress.kubernetes.io/allowlist-source-range contains invalid value",
},
"test parse a empty net": {
net: "",
expectErr: true,
errOut: "the annotation does not contain a valid IP address or network: invalid CIDR address: ",
errOut: "the annotation nginx.ingress.kubernetes.io/allowlist-source-range does not contain a valid value ()",
},
"test parse multiple valid cidr": {
net: "2.2.2.2/32,1.1.1.1/32,3.3.3.0/24",
@ -174,16 +174,67 @@ func TestParseAnnotationsWithDefaultConfig(t *testing.T) {
for testName, test := range tests {
data := map[string]string{}
data[parser.GetAnnotationWithPrefix("whitelist-source-range")] = test.net
data[parser.GetAnnotationWithPrefix(ipAllowlistAnnotation)] = test.net
ing.SetAnnotations(data)
p := NewParser(mockBackend)
i, err := p.Parse(ing)
if err != nil && !test.expectErr {
t.Errorf("%v:unexpected error: %v", testName, err)
if (err != nil) != test.expectErr {
t.Errorf("expected error: %t got error: %t err value: %s. %+v", test.expectErr, err != nil, err, i)
}
if test.expectErr {
if test.expectErr && err != nil {
if err.Error() != test.errOut {
t.Errorf("%v:expected error: %v but %v return", testName, test.errOut, err.Error())
t.Errorf("expected error %s but got %s", test.errOut, err)
}
}
if !test.expectErr {
sr, ok := i.(*SourceRange)
if !ok {
t.Errorf("%v:expected a SourceRange type", testName)
}
if !strsEquals(sr.CIDR, test.expectCidr) {
t.Errorf("%v:expected %v CIDR but %v returned", testName, test.expectCidr, sr.CIDR)
}
}
}
}
// Test that when we have a whitelist set on the Backend that is used when we
// don't have the annotation
func TestLegacyAnnotation(t *testing.T) {
ing := buildIngress()
mockBackend := mockBackend{}
tests := map[string]struct {
net string
expectCidr []string
expectErr bool
errOut string
}{
"test parse a valid net": {
net: "10.0.0.0/24",
expectCidr: []string{"10.0.0.0/24"},
expectErr: false,
},
"test parse multiple valid cidr": {
net: "2.2.2.2/32,1.1.1.1/32,3.3.3.0/24",
expectCidr: []string{"1.1.1.1/32", "2.2.2.2/32", "3.3.3.0/24"},
expectErr: false,
},
}
for testName, test := range tests {
data := map[string]string{}
data[parser.GetAnnotationWithPrefix(ipWhitelistAnnotation)] = test.net
ing.SetAnnotations(data)
p := NewParser(mockBackend)
i, err := p.Parse(ing)
if (err != nil) != test.expectErr {
t.Errorf("expected error: %t got error: %t err value: %s. %+v", test.expectErr, err != nil, err, i)
}
if test.expectErr && err != nil {
if err.Error() != test.errOut {
t.Errorf("expected error %s but got %s", test.errOut, err)
}
}
if !test.expectErr {

View file

@ -30,6 +30,22 @@ import (
"k8s.io/ingress-nginx/pkg/util/sets"
)
const (
ipDenylistAnnotation = "denylist-source-range"
)
var denylistAnnotations = parser.Annotation{
Group: "acl",
Annotations: parser.AnnotationFields{
ipDenylistAnnotation: {
Validator: parser.ValidateCIDRs,
Scope: parser.AnnotationScopeLocation,
Risk: parser.AnnotationRiskMedium, // Failure on parsing this may cause undesired access
Documentation: `This annotation allows setting a list of IPs and networks that should be blocked to access this Location`,
},
},
}
// SourceRange returns the CIDR
type SourceRange struct {
CIDR []string `json:"cidr,omitempty"`
@ -48,12 +64,16 @@ func (sr1 *SourceRange) Equal(sr2 *SourceRange) bool {
}
type ipdenylist struct {
r resolver.Resolver
r resolver.Resolver
annotationConfig parser.Annotation
}
// NewParser creates a new denylist annotation parser
func NewParser(r resolver.Resolver) parser.IngressAnnotation {
return ipdenylist{r}
return ipdenylist{
r: r,
annotationConfig: denylistAnnotations,
}
}
// ParseAnnotations parses the annotations contained in the ingress
@ -67,10 +87,16 @@ func (a ipdenylist) Parse(ing *networking.Ingress) (interface{}, error) {
copy(defaultDenylistSourceRange, defBackend.DenylistSourceRange)
sort.Strings(defaultDenylistSourceRange)
val, err := parser.GetStringAnnotation("denylist-source-range", ing)
// A missing annotation is not a problem, just use the default
if err == ing_errors.ErrMissingAnnotations {
return &SourceRange{CIDR: defaultDenylistSourceRange}, nil
val, err := parser.GetStringAnnotation(ipDenylistAnnotation, ing, a.annotationConfig.Annotations)
if err != nil {
if err == ing_errors.ErrMissingAnnotations {
return &SourceRange{CIDR: defaultDenylistSourceRange}, nil
}
return &SourceRange{CIDR: defaultDenylistSourceRange}, ing_errors.LocationDenied{
Reason: err,
}
}
values := strings.Split(val, ",")
@ -93,3 +119,12 @@ func (a ipdenylist) Parse(ing *networking.Ingress) (interface{}, error) {
return &SourceRange{cidrs}, nil
}
func (a ipdenylist) GetDocumentation() parser.AnnotationFields {
return a.annotationConfig.Annotations
}
func (a ipdenylist) Validate(anns map[string]string) error {
maxrisk := parser.StringRiskToRisk(a.r.GetSecurityConfiguration().AnnotationsRiskLevel)
return parser.CheckAnnotationRisk(anns, maxrisk, denylistAnnotations.Annotations)
}

View file

@ -86,17 +86,17 @@ func TestParseAnnotations(t *testing.T) {
"test parse a invalid net": {
net: "ww",
expectErr: true,
errOut: "the annotation does not contain a valid IP address or network: invalid CIDR address: ww",
errOut: "annotation nginx.ingress.kubernetes.io/denylist-source-range contains invalid value",
},
"test parse a empty net": {
net: "",
expectErr: true,
errOut: "the annotation does not contain a valid IP address or network: invalid CIDR address: ",
errOut: "the annotation nginx.ingress.kubernetes.io/denylist-source-range does not contain a valid value ()",
},
"test parse a malicious escaped string": {
net: `10.0.0.0/8"rm /tmp",11.0.0.0/8`,
expectErr: true,
errOut: `the annotation does not contain a valid IP address or network: invalid CIDR address: 10.0.0.0/8"rm /tmp"`,
errOut: `annotation nginx.ingress.kubernetes.io/denylist-source-range contains invalid value`,
},
"test parse multiple valid cidr": {
net: "2.2.2.2/32,1.1.1.1/32,3.3.3.0/24",
@ -107,16 +107,16 @@ func TestParseAnnotations(t *testing.T) {
for testName, test := range tests {
data := map[string]string{}
data[parser.GetAnnotationWithPrefix("denylist-source-range")] = test.net
data[parser.GetAnnotationWithPrefix(ipDenylistAnnotation)] = test.net
ing.SetAnnotations(data)
p := NewParser(&resolver.Mock{})
i, err := p.Parse(ing)
if err != nil && !test.expectErr {
t.Errorf("%v:unexpected error: %v", testName, err)
if (err != nil) != test.expectErr {
t.Errorf("expected error: %t got error: %t err value: %s. %+v", test.expectErr, err != nil, err, i)
}
if test.expectErr {
if test.expectErr && err != nil {
if err.Error() != test.errOut {
t.Errorf("%v:expected error: %v but %v return", testName, test.errOut, err.Error())
t.Errorf("expected error %s but got %s", test.errOut, err)
}
}
if !test.expectErr {
@ -163,12 +163,12 @@ func TestParseAnnotationsWithDefaultConfig(t *testing.T) {
"test parse a invalid net": {
net: "ww",
expectErr: true,
errOut: "the annotation does not contain a valid IP address or network: invalid CIDR address: ww",
errOut: "annotation nginx.ingress.kubernetes.io/denylist-source-range contains invalid value",
},
"test parse a empty net": {
net: "",
expectErr: true,
errOut: "the annotation does not contain a valid IP address or network: invalid CIDR address: ",
errOut: "the annotation nginx.ingress.kubernetes.io/denylist-source-range does not contain a valid value ()",
},
"test parse multiple valid cidr": {
net: "2.2.2.2/32,1.1.1.1/32,3.3.3.0/24",
@ -179,16 +179,16 @@ func TestParseAnnotationsWithDefaultConfig(t *testing.T) {
for testName, test := range tests {
data := map[string]string{}
data[parser.GetAnnotationWithPrefix("denylist-source-range")] = test.net
data[parser.GetAnnotationWithPrefix(ipDenylistAnnotation)] = test.net
ing.SetAnnotations(data)
p := NewParser(mockBackend)
i, err := p.Parse(ing)
if err != nil && !test.expectErr {
t.Errorf("%v:unexpected error: %v", testName, err)
if (err != nil) != test.expectErr {
t.Errorf("expected error: %t got error: %t err value: %s. %+v", test.expectErr, err != nil, err, i)
}
if test.expectErr {
if test.expectErr && err != nil {
if err.Error() != test.errOut {
t.Errorf("%v:expected error: %v but %v return", testName, test.errOut, err.Error())
t.Errorf("expected error %s but got %s", test.errOut, err)
}
}
if !test.expectErr {

View file

@ -23,18 +23,52 @@ import (
"k8s.io/ingress-nginx/internal/ingress/resolver"
)
type loadbalancing struct {
r resolver.Resolver
// LB Alghorithms are defined in https://github.com/kubernetes/ingress-nginx/blob/d3e75b056f77be54e01bdb18675f1bb46caece31/rootfs/etc/nginx/lua/balancer.lua#L28
const (
loadBalanceAlghoritmAnnotation = "load-balance"
)
var loadBalanceAlghoritms = []string{"round_robin", "chash", "chashsubset", "sticky_balanced", "sticky_persistent", "ewma"}
var loadBalanceAnnotations = parser.Annotation{
Group: "backend",
Annotations: parser.AnnotationFields{
loadBalanceAlghoritmAnnotation: {
Validator: parser.ValidateOptions(loadBalanceAlghoritms, true, true),
Scope: parser.AnnotationScopeLocation,
Risk: parser.AnnotationRiskLow,
Documentation: `This annotation allows setting the load balancing alghorithm that should be used. If none is specified, defaults to
the default configured by Ingress admin, otherwise to round_robin`,
},
},
}
// NewParser creates a new CORS annotation parser
type loadbalancing struct {
r resolver.Resolver
annotationConfig parser.Annotation
}
// NewParser creates a new Load Balancer annotation parser
func NewParser(r resolver.Resolver) parser.IngressAnnotation {
return loadbalancing{r}
return loadbalancing{
r: r,
annotationConfig: loadBalanceAnnotations,
}
}
// Parse parses the annotations contained in the ingress rule
// used to indicate if the location/s contains a fragment of
// configuration to be included inside the paths of the rules
func (a loadbalancing) Parse(ing *networking.Ingress) (interface{}, error) {
return parser.GetStringAnnotation("load-balance", ing)
return parser.GetStringAnnotation(loadBalanceAlghoritmAnnotation, ing, a.annotationConfig.Annotations)
}
func (a loadbalancing) GetDocumentation() parser.AnnotationFields {
return a.annotationConfig.Annotations
}
func (a loadbalancing) Validate(anns map[string]string) error {
maxrisk := parser.StringRiskToRisk(a.r.GetSecurityConfiguration().AnnotationsRiskLevel)
return parser.CheckAnnotationRisk(anns, maxrisk, loadBalanceAnnotations.Annotations)
}

View file

@ -38,7 +38,8 @@ func TestParse(t *testing.T) {
annotations map[string]string
expected string
}{
{map[string]string{annotation: "ip_hash"}, "ip_hash"},
{map[string]string{annotation: "ewma"}, "ewma"},
{map[string]string{annotation: "ip_hash"}, ""}, // This is invalid and should not return anything
{map[string]string{}, ""},
{nil, ""},
}

View file

@ -23,8 +23,32 @@ import (
"k8s.io/ingress-nginx/internal/ingress/resolver"
)
const (
enableAccessLogAnnotation = "enable-access-log"
enableRewriteLogAnnotation = "enable-rewrite-log"
)
var logAnnotations = parser.Annotation{
Group: "log",
Annotations: parser.AnnotationFields{
enableAccessLogAnnotation: {
Validator: parser.ValidateBool,
Scope: parser.AnnotationScopeLocation,
Risk: parser.AnnotationRiskLow,
Documentation: `This configuration setting allows you to control if this location should generate an access_log`,
},
enableRewriteLogAnnotation: {
Validator: parser.ValidateBool,
Scope: parser.AnnotationScopeLocation,
Risk: parser.AnnotationRiskLow,
Documentation: `This configuration setting allows you to control if this location should generate logs from the rewrite feature usage`,
},
},
}
type log struct {
r resolver.Resolver
r resolver.Resolver
annotationConfig parser.Annotation
}
// Config contains the configuration to be used in the Ingress
@ -48,7 +72,10 @@ func (bd1 *Config) Equal(bd2 *Config) bool {
// NewParser creates a new log annotations parser
func NewParser(r resolver.Resolver) parser.IngressAnnotation {
return log{r}
return log{
r: r,
annotationConfig: logAnnotations,
}
}
// Parse parses the annotations contained in the ingress
@ -57,15 +84,24 @@ func (l log) Parse(ing *networking.Ingress) (interface{}, error) {
var err error
config := &Config{}
config.Access, err = parser.GetBoolAnnotation("enable-access-log", ing)
config.Access, err = parser.GetBoolAnnotation(enableAccessLogAnnotation, ing, l.annotationConfig.Annotations)
if err != nil {
config.Access = true
}
config.Rewrite, err = parser.GetBoolAnnotation("enable-rewrite-log", ing)
config.Rewrite, err = parser.GetBoolAnnotation(enableRewriteLogAnnotation, ing, l.annotationConfig.Annotations)
if err != nil {
config.Rewrite = false
}
return config, nil
}
func (l log) GetDocumentation() parser.AnnotationFields {
return l.annotationConfig.Annotations
}
func (a log) Validate(anns map[string]string) error {
maxrisk := parser.StringRiskToRisk(a.r.GetSecurityConfiguration().AnnotationsRiskLevel)
return parser.CheckAnnotationRisk(anns, maxrisk, logAnnotations.Annotations)
}

View file

@ -73,7 +73,7 @@ func TestIngressAccessLogConfig(t *testing.T) {
ing := buildIngress()
data := map[string]string{}
data[parser.GetAnnotationWithPrefix("enable-access-log")] = "false"
data[parser.GetAnnotationWithPrefix(enableAccessLogAnnotation)] = "false"
ing.SetAnnotations(data)
log, _ := NewParser(&resolver.Mock{}).Parse(ing)
@ -91,7 +91,7 @@ func TestIngressRewriteLogConfig(t *testing.T) {
ing := buildIngress()
data := map[string]string{}
data[parser.GetAnnotationWithPrefix("enable-rewrite-log")] = "true"
data[parser.GetAnnotationWithPrefix(enableRewriteLogAnnotation)] = "true"
ing.SetAnnotations(data)
log, _ := NewParser(&resolver.Mock{}).Parse(ing)
@ -104,3 +104,21 @@ func TestIngressRewriteLogConfig(t *testing.T) {
t.Errorf("expected rewrite log to be enabled but it is disabled")
}
}
func TestInvalidBoolConfig(t *testing.T) {
ing := buildIngress()
data := map[string]string{}
data[parser.GetAnnotationWithPrefix(enableRewriteLogAnnotation)] = "blo"
ing.SetAnnotations(data)
log, _ := NewParser(&resolver.Mock{}).Parse(ing)
nginxLogs, ok := log.(*Config)
if !ok {
t.Errorf("expected a Config type")
}
if !nginxLogs.Access {
t.Errorf("expected access log to be enabled due to invalid config, but it is disabled")
}
}

View file

@ -18,13 +18,50 @@ package mirror
import (
"fmt"
"regexp"
"strings"
networking "k8s.io/api/networking/v1"
"k8s.io/ingress-nginx/internal/ingress/annotations/parser"
"k8s.io/ingress-nginx/internal/ingress/errors"
"k8s.io/ingress-nginx/internal/ingress/resolver"
"k8s.io/klog/v2"
)
const (
mirrorRequestBodyAnnotation = "mirror-request-body"
mirrorTargetAnnotation = "mirror-target"
mirrorHostAnnotation = "mirror-host"
)
var (
OnOffRegex = regexp.MustCompile(`^(on|off)$`)
)
var mirrorAnnotation = parser.Annotation{
Group: "mirror",
Annotations: parser.AnnotationFields{
mirrorRequestBodyAnnotation: {
Validator: parser.ValidateRegex(*OnOffRegex, true),
Scope: parser.AnnotationScopeIngress,
Risk: parser.AnnotationRiskLow,
Documentation: `This annotation defines if the request-body should be sent to the mirror backend. Can be 'on' or 'off'`,
},
mirrorTargetAnnotation: {
Validator: parser.ValidateServerName,
Scope: parser.AnnotationScopeIngress,
Risk: parser.AnnotationRiskHigh,
Documentation: `This annotation enables a request to be mirrored to a mirror backend.`,
},
mirrorHostAnnotation: {
Validator: parser.ValidateServerName,
Scope: parser.AnnotationScopeIngress,
Risk: parser.AnnotationRiskHigh,
Documentation: `This annotation defines if a specific Host header should be set for mirrored request.`,
},
},
}
// Config returns the mirror to use in a given location
type Config struct {
Source string `json:"source"`
@ -63,12 +100,16 @@ func (m1 *Config) Equal(m2 *Config) bool {
}
type mirror struct {
r resolver.Resolver
r resolver.Resolver
annotationConfig parser.Annotation
}
// NewParser creates a new mirror configuration annotation parser
func NewParser(r resolver.Resolver) parser.IngressAnnotation {
return mirror{r}
return mirror{
r: r,
annotationConfig: mirrorAnnotation,
}
}
// ParseAnnotations parses the annotations contained in the ingress
@ -79,19 +120,29 @@ func (a mirror) Parse(ing *networking.Ingress) (interface{}, error) {
}
var err error
config.RequestBody, err = parser.GetStringAnnotation("mirror-request-body", ing)
config.RequestBody, err = parser.GetStringAnnotation(mirrorRequestBodyAnnotation, ing, a.annotationConfig.Annotations)
if err != nil || config.RequestBody != "off" {
if errors.IsValidationError(err) {
klog.Warningf("annotation %s contains invalid value", mirrorRequestBodyAnnotation)
}
config.RequestBody = "on"
}
config.Target, err = parser.GetStringAnnotation("mirror-target", ing)
config.Target, err = parser.GetStringAnnotation(mirrorTargetAnnotation, ing, a.annotationConfig.Annotations)
if err != nil {
config.Target = ""
config.Source = ""
if errors.IsValidationError(err) {
klog.Warningf("annotation %s contains invalid value, defaulting", mirrorTargetAnnotation)
} else {
config.Target = ""
config.Source = ""
}
}
config.Host, err = parser.GetStringAnnotation("mirror-host", ing)
config.Host, err = parser.GetStringAnnotation(mirrorHostAnnotation, ing, a.annotationConfig.Annotations)
if err != nil {
if errors.IsValidationError(err) {
klog.Warningf("annotation %s contains invalid value, defaulting", mirrorHostAnnotation)
}
if config.Target != "" {
target := strings.Split(config.Target, "$")
@ -106,3 +157,12 @@ func (a mirror) Parse(ing *networking.Ingress) (interface{}, error) {
return config, nil
}
func (a mirror) GetDocumentation() parser.AnnotationFields {
return a.annotationConfig.Annotations
}
func (a mirror) Validate(anns map[string]string) error {
maxrisk := parser.StringRiskToRisk(a.r.GetSecurityConfiguration().AnnotationsRiskLevel)
return parser.CheckAnnotationRisk(anns, maxrisk, mirrorAnnotation.Annotations)
}

View file

@ -94,13 +94,13 @@ func TestParse(t *testing.T) {
Source: ngxURI,
RequestBody: "on",
Target: "http://some.test.env.com",
Host: "someInvalidParm.%^&*()_=!@#'\"",
Host: "some.test.env.com",
}},
{map[string]string{backendURL: "http://some.test.env.com", host: "_sbrubles-i\"@xpto:12345"}, &Config{
Source: ngxURI,
RequestBody: "on",
Target: "http://some.test.env.com",
Host: "_sbrubles-i\"@xpto:12345",
Host: "some.test.env.com",
}},
}
@ -115,9 +115,12 @@ func TestParse(t *testing.T) {
for _, testCase := range testCases {
ing.SetAnnotations(testCase.annotations)
result, _ := ap.Parse(ing)
result, err := ap.Parse(ing)
if err != nil {
t.Errorf(err.Error())
}
if !reflect.DeepEqual(result, testCase.expected) {
t.Errorf("expected %v but returned %v, annotations: %s", testCase.expected, result, testCase.annotations)
t.Errorf("expected %+v but returned %+v, annotations: %s", testCase.expected, result, testCase.annotations)
}
}
}

View file

@ -19,9 +19,48 @@ package modsecurity
import (
networking "k8s.io/api/networking/v1"
"k8s.io/ingress-nginx/internal/ingress/annotations/parser"
"k8s.io/ingress-nginx/internal/ingress/errors"
"k8s.io/ingress-nginx/internal/ingress/resolver"
"k8s.io/klog/v2"
)
const (
modsecEnableAnnotation = "enable-modsecurity"
modsecEnableOwaspCoreAnnotation = "enable-owasp-core-rules"
modesecTransactionIdAnnotation = "modsecurity-transaction-id"
modsecSnippetAnnotation = "modsecurity-snippet"
)
var modsecurityAnnotation = parser.Annotation{
Group: "modsecurity",
Annotations: parser.AnnotationFields{
modsecEnableAnnotation: {
Validator: parser.ValidateBool,
Scope: parser.AnnotationScopeIngress,
Risk: parser.AnnotationRiskLow,
Documentation: `This annotation enables ModSecurity`,
},
modsecEnableOwaspCoreAnnotation: {
Validator: parser.ValidateBool,
Scope: parser.AnnotationScopeIngress,
Risk: parser.AnnotationRiskLow,
Documentation: `This annotation enables the OWASP Core Rule Set`,
},
modesecTransactionIdAnnotation: {
Validator: parser.ValidateRegex(*parser.NGINXVariable, true),
Scope: parser.AnnotationScopeIngress,
Risk: parser.AnnotationRiskHigh,
Documentation: `This annotation enables passing an NGINX variable to ModSecurity.`,
},
modsecSnippetAnnotation: {
Validator: parser.ValidateNull,
Scope: parser.AnnotationScopeIngress,
Risk: parser.AnnotationRiskCritical,
Documentation: `This annotation enables adding a specific snippet configuration for ModSecurity`,
},
},
}
// Config contains ModSecurity Configuration items
type Config struct {
Enable bool `json:"enable-modsecurity"`
@ -60,11 +99,15 @@ func (modsec1 *Config) Equal(modsec2 *Config) bool {
// NewParser creates a new ModSecurity annotation parser
func NewParser(resolver resolver.Resolver) parser.IngressAnnotation {
return modSecurity{resolver}
return modSecurity{
r: resolver,
annotationConfig: modsecurityAnnotation,
}
}
type modSecurity struct {
r resolver.Resolver
r resolver.Resolver
annotationConfig parser.Annotation
}
// Parse parses the annotations contained in the ingress
@ -74,26 +117,44 @@ func (a modSecurity) Parse(ing *networking.Ingress) (interface{}, error) {
config := &Config{}
config.EnableSet = true
config.Enable, err = parser.GetBoolAnnotation("enable-modsecurity", ing)
config.Enable, err = parser.GetBoolAnnotation(modsecEnableAnnotation, ing, a.annotationConfig.Annotations)
if err != nil {
if errors.IsInvalidContent(err) {
klog.Warningf("annotation %s contains invalid directive, defaulting to false", modsecEnableAnnotation)
}
config.Enable = false
config.EnableSet = false
}
config.OWASPRules, err = parser.GetBoolAnnotation("enable-owasp-core-rules", ing)
config.OWASPRules, err = parser.GetBoolAnnotation(modsecEnableOwaspCoreAnnotation, ing, a.annotationConfig.Annotations)
if err != nil {
if errors.IsInvalidContent(err) {
klog.Warningf("annotation %s contains invalid directive, defaulting to false", modsecEnableOwaspCoreAnnotation)
}
config.OWASPRules = false
}
config.TransactionID, err = parser.GetStringAnnotation("modsecurity-transaction-id", ing)
config.TransactionID, err = parser.GetStringAnnotation(modesecTransactionIdAnnotation, ing, a.annotationConfig.Annotations)
if err != nil {
if errors.IsInvalidContent(err) {
klog.Warningf("annotation %s contains invalid directive, defaulting", modesecTransactionIdAnnotation)
}
config.TransactionID = ""
}
config.Snippet, err = parser.GetStringAnnotation("modsecurity-snippet", ing)
config.Snippet, err = parser.GetStringAnnotation("modsecurity-snippet", ing, a.annotationConfig.Annotations)
if err != nil {
config.Snippet = ""
}
return config, nil
}
func (a modSecurity) GetDocumentation() parser.AnnotationFields {
return a.annotationConfig.Annotations
}
func (a modSecurity) Validate(anns map[string]string) error {
maxrisk := parser.StringRiskToRisk(a.r.GetSecurityConfiguration().AnnotationsRiskLevel)
return parser.CheckAnnotationRisk(anns, maxrisk, modsecurityAnnotation.Annotations)
}

View file

@ -17,14 +17,51 @@ limitations under the License.
package opentelemetry
import (
"regexp"
networking "k8s.io/api/networking/v1"
"k8s.io/ingress-nginx/internal/ingress/annotations/parser"
"k8s.io/ingress-nginx/internal/ingress/errors"
"k8s.io/ingress-nginx/internal/ingress/resolver"
)
const (
enableOpenTelemetryAnnotation = "enable-opentelemetry"
otelTrustSpanAnnotation = "opentelemetry-trust-incoming-span"
otelOperationNameAnnotation = "opentelemetry-operation-name"
)
var regexOperationName = regexp.MustCompile(`^[A-Za-z0-9_\-]*$`)
var otelAnnotations = parser.Annotation{
Group: "opentelemetry",
Annotations: parser.AnnotationFields{
enableOpenTelemetryAnnotation: {
Validator: parser.ValidateBool,
Scope: parser.AnnotationScopeLocation,
Risk: parser.AnnotationRiskLow,
Documentation: `This annotation defines if Open Telemetry collector should be enable for this location. OpenTelemetry should
already be configured by Ingress administrator`,
},
otelTrustSpanAnnotation: {
Validator: parser.ValidateBool,
Scope: parser.AnnotationScopeLocation,
Risk: parser.AnnotationRiskLow,
Documentation: `This annotation enables or disables using spans from incoming requests as parent for created ones`,
},
otelOperationNameAnnotation: {
Validator: parser.ValidateRegex(*regexOperationName, true),
Scope: parser.AnnotationScopeLocation,
Risk: parser.AnnotationRiskMedium,
Documentation: `This annotation defines what operation name should be added to the span`,
},
},
}
type opentelemetry struct {
r resolver.Resolver
r resolver.Resolver
annotationConfig parser.Annotation
}
// Config contains the configuration to be used in the Ingress
@ -64,13 +101,16 @@ func (bd1 *Config) Equal(bd2 *Config) bool {
// NewParser creates a new serviceUpstream annotation parser
func NewParser(r resolver.Resolver) parser.IngressAnnotation {
return opentelemetry{r}
return opentelemetry{
r: r,
annotationConfig: otelAnnotations,
}
}
// Parse parses the annotations to look for opentelemetry configurations
func (c opentelemetry) Parse(ing *networking.Ingress) (interface{}, error) {
cfg := Config{}
enabled, err := parser.GetBoolAnnotation("enable-opentelemetry", ing)
enabled, err := parser.GetBoolAnnotation(enableOpenTelemetryAnnotation, ing, c.annotationConfig.Annotations)
if err != nil {
return &cfg, nil
}
@ -80,10 +120,13 @@ func (c opentelemetry) Parse(ing *networking.Ingress) (interface{}, error) {
return &cfg, nil
}
trustEnabled, err := parser.GetBoolAnnotation("opentelemetry-trust-incoming-span", ing)
trustEnabled, err := parser.GetBoolAnnotation(otelTrustSpanAnnotation, ing, c.annotationConfig.Annotations)
if err != nil {
operationName, err := parser.GetStringAnnotation("opentelemetry-operation-name", ing)
operationName, err := parser.GetStringAnnotation(otelOperationNameAnnotation, ing, c.annotationConfig.Annotations)
if err != nil {
if errors.IsValidationError(err) {
return nil, err
}
return &cfg, nil
}
cfg.OperationName = operationName
@ -92,10 +135,22 @@ func (c opentelemetry) Parse(ing *networking.Ingress) (interface{}, error) {
cfg.TrustSet = true
cfg.TrustEnabled = trustEnabled
operationName, err := parser.GetStringAnnotation("opentelemetry-operation-name", ing)
operationName, err := parser.GetStringAnnotation(otelOperationNameAnnotation, ing, c.annotationConfig.Annotations)
if err != nil {
if errors.IsValidationError(err) {
return nil, err
}
return &cfg, nil
}
cfg.OperationName = operationName
return &cfg, nil
}
func (c opentelemetry) GetDocumentation() parser.AnnotationFields {
return c.annotationConfig.Annotations
}
func (a opentelemetry) Validate(anns map[string]string) error {
maxrisk := parser.StringRiskToRisk(a.r.GetSecurityConfiguration().AnnotationsRiskLevel)
return parser.CheckAnnotationRisk(anns, maxrisk, otelAnnotations.Annotations)
}

View file

@ -73,7 +73,7 @@ func TestIngressAnnotationOpentelemetrySetTrue(t *testing.T) {
ing := buildIngress()
data := map[string]string{}
data[parser.GetAnnotationWithPrefix("enable-opentelemetry")] = "true"
data[parser.GetAnnotationWithPrefix(enableOpenTelemetryAnnotation)] = "true"
ing.SetAnnotations(data)
val, _ := NewParser(&resolver.Mock{}).Parse(ing)
@ -100,7 +100,7 @@ func TestIngressAnnotationOpentelemetrySetFalse(t *testing.T) {
// Test with explicitly set to false
data := map[string]string{}
data[parser.GetAnnotationWithPrefix("enable-opentelemetry")] = "false"
data[parser.GetAnnotationWithPrefix(enableOpenTelemetryAnnotation)] = "false"
ing.SetAnnotations(data)
val, _ := NewParser(&resolver.Mock{}).Parse(ing)
@ -123,12 +123,15 @@ func TestIngressAnnotationOpentelemetryTrustSetTrue(t *testing.T) {
data := map[string]string{}
opName := "foo-op"
data[parser.GetAnnotationWithPrefix("enable-opentelemetry")] = "true"
data[parser.GetAnnotationWithPrefix("opentelemetry-trust-incoming-span")] = "true"
data[parser.GetAnnotationWithPrefix("opentelemetry-operation-name")] = opName
data[parser.GetAnnotationWithPrefix(enableOpenTelemetryAnnotation)] = "true"
data[parser.GetAnnotationWithPrefix(otelTrustSpanAnnotation)] = "true"
data[parser.GetAnnotationWithPrefix(otelOperationNameAnnotation)] = opName
ing.SetAnnotations(data)
val, _ := NewParser(&resolver.Mock{}).Parse(ing)
val, err := NewParser(&resolver.Mock{}).Parse(ing)
if err != nil {
t.Fatal(err)
}
openTelemetry, ok := val.(*Config)
if !ok {
t.Errorf("expected a Config type")
@ -155,6 +158,21 @@ func TestIngressAnnotationOpentelemetryTrustSetTrue(t *testing.T) {
}
}
func TestIngressAnnotationOpentelemetryWithBadOpName(t *testing.T) {
ing := buildIngress()
data := map[string]string{}
opName := "fooxpto_123$la;"
data[parser.GetAnnotationWithPrefix(enableOpenTelemetryAnnotation)] = "true"
data[parser.GetAnnotationWithPrefix(otelOperationNameAnnotation)] = opName
ing.SetAnnotations(data)
_, err := NewParser(&resolver.Mock{}).Parse(ing)
if err == nil {
t.Fatalf("This operation should return an error but no error was returned")
}
}
func TestIngressAnnotationOpentelemetryUnset(t *testing.T) {
ing := buildIngress()

View file

@ -23,8 +23,33 @@ import (
"k8s.io/ingress-nginx/internal/ingress/resolver"
)
const (
enableOpentracingAnnotation = "enable-opentracing"
opentracingTrustSpanAnnotation = "opentracing-trust-incoming-span"
)
var opentracingAnnotations = parser.Annotation{
Group: "opentracing",
Annotations: parser.AnnotationFields{
enableOpentracingAnnotation: {
Validator: parser.ValidateBool,
Scope: parser.AnnotationScopeLocation,
Risk: parser.AnnotationRiskLow,
Documentation: `This annotation defines if Opentracing collector should be enable for this location. Opentracing should
already be configured by Ingress administrator`,
},
opentracingTrustSpanAnnotation: {
Validator: parser.ValidateBool,
Scope: parser.AnnotationScopeLocation,
Risk: parser.AnnotationRiskLow,
Documentation: `This annotation enables or disables using spans from incoming requests as parent for created ones`,
},
},
}
type opentracing struct {
r resolver.Resolver
r resolver.Resolver
annotationConfig parser.Annotation
}
// Config contains the configuration to be used in the Ingress
@ -58,19 +83,31 @@ func (bd1 *Config) Equal(bd2 *Config) bool {
// NewParser creates a new serviceUpstream annotation parser
func NewParser(r resolver.Resolver) parser.IngressAnnotation {
return opentracing{r}
return opentracing{
r: r,
annotationConfig: opentracingAnnotations,
}
}
func (s opentracing) Parse(ing *networking.Ingress) (interface{}, error) {
enabled, err := parser.GetBoolAnnotation("enable-opentracing", ing)
enabled, err := parser.GetBoolAnnotation(enableOpentracingAnnotation, ing, s.annotationConfig.Annotations)
if err != nil {
return &Config{}, nil
}
trustSpan, err := parser.GetBoolAnnotation("opentracing-trust-incoming-span", ing)
trustSpan, err := parser.GetBoolAnnotation(opentracingTrustSpanAnnotation, ing, s.annotationConfig.Annotations)
if err != nil {
return &Config{Set: true, Enabled: enabled}, nil
}
return &Config{Set: true, Enabled: enabled, TrustSet: true, TrustEnabled: trustSpan}, nil
}
func (s opentracing) GetDocumentation() parser.AnnotationFields {
return s.annotationConfig.Annotations
}
func (a opentracing) Validate(anns map[string]string) error {
maxrisk := parser.StringRiskToRisk(a.r.GetSecurityConfiguration().AnnotationsRiskLevel)
return parser.CheckAnnotationRisk(anns, maxrisk, opentracingAnnotations.Annotations)
}

View file

@ -73,7 +73,7 @@ func TestIngressAnnotationOpentracingSetTrue(t *testing.T) {
ing := buildIngress()
data := map[string]string{}
data[parser.GetAnnotationWithPrefix("enable-opentracing")] = "true"
data[parser.GetAnnotationWithPrefix(enableOpentracingAnnotation)] = "true"
ing.SetAnnotations(data)
val, _ := NewParser(&resolver.Mock{}).Parse(ing)
@ -92,7 +92,7 @@ func TestIngressAnnotationOpentracingSetFalse(t *testing.T) {
// Test with explicitly set to false
data := map[string]string{}
data[parser.GetAnnotationWithPrefix("enable-opentracing")] = "false"
data[parser.GetAnnotationWithPrefix(enableOpentracingAnnotation)] = "false"
ing.SetAnnotations(data)
val, _ := NewParser(&resolver.Mock{}).Parse(ing)
@ -110,8 +110,8 @@ func TestIngressAnnotationOpentracingTrustSetTrue(t *testing.T) {
ing := buildIngress()
data := map[string]string{}
data[parser.GetAnnotationWithPrefix("enable-opentracing")] = "true"
data[parser.GetAnnotationWithPrefix("opentracing-trust-incoming-span")] = "true"
data[parser.GetAnnotationWithPrefix(enableOpentracingAnnotation)] = "true"
data[parser.GetAnnotationWithPrefix(opentracingTrustSpanAnnotation)] = "true"
ing.SetAnnotations(data)
val, _ := NewParser(&resolver.Mock{}).Parse(ing)

View file

@ -29,20 +29,79 @@ import (
)
// DefaultAnnotationsPrefix defines the common prefix used in the nginx ingress controller
const DefaultAnnotationsPrefix = "nginx.ingress.kubernetes.io"
const (
DefaultAnnotationsPrefix = "nginx.ingress.kubernetes.io"
DefaultEnableAnnotationValidation = true
)
var (
// AnnotationsPrefix is the mutable attribute that the controller explicitly refers to
AnnotationsPrefix = DefaultAnnotationsPrefix
// Enable is the mutable attribute for enabling or disabling the validation functions
EnableAnnotationValidation = DefaultEnableAnnotationValidation
)
// AnnotationGroup defines the group that this annotation may belong
// eg.: Security, Snippets, Rewrite, etc
type AnnotationGroup string
// AnnotationScope defines which scope this annotation applies. May be to the whole
// ingress, per location, etc
type AnnotationScope string
var (
AnnotationScopeLocation AnnotationScope = "location"
AnnotationScopeIngress AnnotationScope = "ingress"
)
// AnnotationRisk is a subset of risk that an annotation may represent.
// Based on the Risk, the admin will be able to allow or disallow users to set it
// on their ingress objects
type AnnotationRisk int
type AnnotationFields map[string]AnnotationConfig
// AnnotationConfig defines the configuration that a single annotation field
// has, with the Validator and the documentation of this field.
type AnnotationConfig struct {
// Validator defines a function to validate the annotation value
Validator AnnotationValidator
// Documentation defines a user facing documentation for this annotation. This
// field will be used to auto generate documentations
Documentation string
// Risk defines a risk of this annotation being exposed to the user. Annotations
// with bool fields, or to set timeout are usually low risk. Annotations that allows
// string input without a limited set of options may represent a high risk
Risk AnnotationRisk
// Scope defines which scope this annotation applies, may be to location, to an Ingress object, etc
Scope AnnotationScope
// AnnotationAliases defines other names this annotation may have.
AnnotationAliases []string
}
// Annotation defines an annotation feature an Ingress may have.
// It should contain the internal resolver, and all the annotations
// with configs and Validators that should be used for each Annotation
type Annotation struct {
// Annotations contains all the annotations that belong to this feature
Annotations AnnotationFields
// Group defines which annotation group this feature belongs to
Group AnnotationGroup
}
// IngressAnnotation has a method to parse annotations located in Ingress
type IngressAnnotation interface {
Parse(ing *networking.Ingress) (interface{}, error)
GetDocumentation() AnnotationFields
Validate(anns map[string]string) error
}
type ingAnnotations map[string]string
// TODO: We already parse all of this on checkAnnotation and can just do a parse over the
// value
func (a ingAnnotations) parseBool(name string) (bool, error) {
val, ok := a[name]
if ok {
@ -92,21 +151,9 @@ func (a ingAnnotations) parseFloat32(name string) (float32, error) {
return 0, errors.ErrMissingAnnotations
}
func checkAnnotation(name string, ing *networking.Ingress) error {
if ing == nil || len(ing.GetAnnotations()) == 0 {
return errors.ErrMissingAnnotations
}
if name == "" {
return errors.ErrInvalidAnnotationName
}
return nil
}
// GetBoolAnnotation extracts a boolean from an Ingress annotation
func GetBoolAnnotation(name string, ing *networking.Ingress) (bool, error) {
v := GetAnnotationWithPrefix(name)
err := checkAnnotation(v, ing)
func GetBoolAnnotation(name string, ing *networking.Ingress, fields AnnotationFields) (bool, error) {
v, err := checkAnnotation(name, ing, fields)
if err != nil {
return false, err
}
@ -114,9 +161,8 @@ func GetBoolAnnotation(name string, ing *networking.Ingress) (bool, error) {
}
// GetStringAnnotation extracts a string from an Ingress annotation
func GetStringAnnotation(name string, ing *networking.Ingress) (string, error) {
v := GetAnnotationWithPrefix(name)
err := checkAnnotation(v, ing)
func GetStringAnnotation(name string, ing *networking.Ingress, fields AnnotationFields) (string, error) {
v, err := checkAnnotation(name, ing, fields)
if err != nil {
return "", err
}
@ -125,9 +171,8 @@ func GetStringAnnotation(name string, ing *networking.Ingress) (string, error) {
}
// GetIntAnnotation extracts an int from an Ingress annotation
func GetIntAnnotation(name string, ing *networking.Ingress) (int, error) {
v := GetAnnotationWithPrefix(name)
err := checkAnnotation(v, ing)
func GetIntAnnotation(name string, ing *networking.Ingress, fields AnnotationFields) (int, error) {
v, err := checkAnnotation(name, ing, fields)
if err != nil {
return 0, err
}
@ -135,9 +180,8 @@ func GetIntAnnotation(name string, ing *networking.Ingress) (int, error) {
}
// GetFloatAnnotation extracts a float32 from an Ingress annotation
func GetFloatAnnotation(name string, ing *networking.Ingress) (float32, error) {
v := GetAnnotationWithPrefix(name)
err := checkAnnotation(v, ing)
func GetFloatAnnotation(name string, ing *networking.Ingress, fields AnnotationFields) (float32, error) {
v, err := checkAnnotation(name, ing, fields)
if err != nil {
return 0, err
}
@ -149,6 +193,23 @@ func GetAnnotationWithPrefix(suffix string) string {
return fmt.Sprintf("%v/%v", AnnotationsPrefix, suffix)
}
func TrimAnnotationPrefix(annotation string) string {
return strings.TrimPrefix(annotation, AnnotationsPrefix+"/")
}
func StringRiskToRisk(risk string) AnnotationRisk {
switch strings.ToLower(risk) {
case "critical":
return AnnotationRiskCritical
case "high":
return AnnotationRiskHigh
case "medium":
return AnnotationRiskMedium
default:
return AnnotationRiskLow
}
}
func normalizeString(input string) string {
trimmedContent := []string{}
for _, line := range strings.Split(input, "\n") {

View file

@ -38,7 +38,7 @@ func buildIngress() *networking.Ingress {
func TestGetBoolAnnotation(t *testing.T) {
ing := buildIngress()
_, err := GetBoolAnnotation("", nil)
_, err := GetBoolAnnotation("", nil, nil)
if err == nil {
t.Errorf("expected error but retuned nil")
}
@ -59,8 +59,8 @@ func TestGetBoolAnnotation(t *testing.T) {
for _, test := range tests {
data[GetAnnotationWithPrefix(test.field)] = test.value
u, err := GetBoolAnnotation(test.field, ing)
ing.SetAnnotations(data)
u, err := GetBoolAnnotation(test.field, ing, nil)
if test.expErr {
if err == nil {
t.Errorf("%v: expected error but retuned nil", test.name)
@ -68,7 +68,7 @@ func TestGetBoolAnnotation(t *testing.T) {
continue
}
if u != test.exp {
t.Errorf("%v: expected \"%v\" but \"%v\" was returned", test.name, test.exp, u)
t.Errorf("%v: expected \"%v\" but \"%v\" was returned, %+v", test.name, test.exp, u, ing)
}
delete(data, test.field)
@ -78,7 +78,7 @@ func TestGetBoolAnnotation(t *testing.T) {
func TestGetStringAnnotation(t *testing.T) {
ing := buildIngress()
_, err := GetStringAnnotation("", nil)
_, err := GetStringAnnotation("", nil, nil)
if err == nil {
t.Errorf("expected error but none returned")
}
@ -109,7 +109,7 @@ rewrite (?i)/arcgis/services/Utilities/Geometry/GeometryServer(.*)$ /arcgis/serv
for _, test := range tests {
data[GetAnnotationWithPrefix(test.field)] = test.value
s, err := GetStringAnnotation(test.field, ing)
s, err := GetStringAnnotation(test.field, ing, nil)
if test.expErr {
if err == nil {
t.Errorf("%v: expected error but none returned", test.name)
@ -133,7 +133,7 @@ rewrite (?i)/arcgis/services/Utilities/Geometry/GeometryServer(.*)$ /arcgis/serv
func TestGetFloatAnnotation(t *testing.T) {
ing := buildIngress()
_, err := GetFloatAnnotation("", nil)
_, err := GetFloatAnnotation("", nil, nil)
if err == nil {
t.Errorf("expected error but retuned nil")
}
@ -156,7 +156,7 @@ func TestGetFloatAnnotation(t *testing.T) {
for _, test := range tests {
data[GetAnnotationWithPrefix(test.field)] = test.value
s, err := GetFloatAnnotation(test.field, ing)
s, err := GetFloatAnnotation(test.field, ing, nil)
if test.expErr {
if err == nil {
t.Errorf("%v: expected error but retuned nil", test.name)
@ -174,7 +174,7 @@ func TestGetFloatAnnotation(t *testing.T) {
func TestGetIntAnnotation(t *testing.T) {
ing := buildIngress()
_, err := GetIntAnnotation("", nil)
_, err := GetIntAnnotation("", nil, nil)
if err == nil {
t.Errorf("expected error but retuned nil")
}
@ -196,7 +196,7 @@ func TestGetIntAnnotation(t *testing.T) {
for _, test := range tests {
data[GetAnnotationWithPrefix(test.field)] = test.value
s, err := GetIntAnnotation(test.field, ing)
s, err := GetIntAnnotation(test.field, ing, nil)
if test.expErr {
if err == nil {
t.Errorf("%v: expected error but retuned nil", test.name)
@ -224,6 +224,7 @@ func TestStringToURL(t *testing.T) {
}{
{"empty", "", "url scheme is empty", nil, true},
{"no scheme", "bar", "url scheme is empty", nil, true},
{"invalid parse", "://lala.com", "://lala.com is not a valid URL: parse \"://lala.com\": missing protocol scheme", nil, true},
{"invalid host", "http://", "url host is empty", nil, true},
{"invalid host (multiple dots)", "http://foo..bar.com", "invalid url host", nil, true},
{"valid URL", validURL, "", validParsedURL, false},

View file

@ -0,0 +1,239 @@
/*
Copyright 2023 The Kubernetes Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package parser
import (
"errors"
"fmt"
"regexp"
"strconv"
"strings"
"time"
networking "k8s.io/api/networking/v1"
machineryvalidation "k8s.io/apimachinery/pkg/api/validation"
ing_errors "k8s.io/ingress-nginx/internal/ingress/errors"
"k8s.io/ingress-nginx/internal/net"
"k8s.io/klog/v2"
)
type AnnotationValidator func(string) error
const (
AnnotationRiskLow AnnotationRisk = iota
AnnotationRiskMedium
AnnotationRiskHigh
AnnotationRiskCritical
)
var (
alphaNumericChars = `\-\.\_\~a-zA-Z0-9\/:`
extendedAlphaNumeric = alphaNumericChars + ", "
regexEnabledChars = regexp.QuoteMeta(`^$[](){}*+?|&=\`)
urlEnabledChars = regexp.QuoteMeta(`:?&=`)
)
// IsValidRegex checks if the tested string can be used as a regex, but without any weird character.
// It includes regex characters for paths that may contain regexes
var IsValidRegex = regexp.MustCompile("^[/" + alphaNumericChars + regexEnabledChars + "]*$")
// SizeRegex validates sizes understood by NGINX, like 1000, 100k, 1000M
var SizeRegex = regexp.MustCompile("^(?i)[0-9]+[bkmg]?$")
// URLRegex is used to validate a URL but with only a specific set of characters:
// It is alphanumericChar + ":", "?", "&"
// A valid URL would be proto://something.com:port/something?arg=param
var (
// URLIsValidRegex is used on full URLs, containing query strings (:, ? and &)
URLIsValidRegex = regexp.MustCompile("^[" + alphaNumericChars + urlEnabledChars + "]*$")
// BasicChars is alphanumeric and ".", "-", "_", "~" and ":", usually used on simple host:port/path composition.
// This combination can also be used on fields that may contain characters like / (as ns/name)
BasicCharsRegex = regexp.MustCompile("^[/" + alphaNumericChars + "]*$")
// ExtendedChars is alphanumeric and ".", "-", "_", "~" and ":" plus "," and spaces, usually used on simple host:port/path composition
ExtendedCharsRegex = regexp.MustCompile("^[/" + extendedAlphaNumeric + "]*$")
// CharsWithSpace is like basic chars, but includes the space character
CharsWithSpace = regexp.MustCompile("^[/" + alphaNumericChars + " ]*$")
// NGINXVariable allows entries with alphanumeric characters, -, _ and the special "$"
NGINXVariable = regexp.MustCompile(`^[A-Za-z0-9\-\_\$\{\}]*$`)
// RegexPathWithCapture allows entries that SHOULD start with "/" and may contain alphanumeric + capture
// character for regex based paths, like /something/$1/anything/$2
RegexPathWithCapture = regexp.MustCompile(`^/[` + alphaNumericChars + `\/\$]*$`)
// HeadersVariable defines a regex that allows headers separated by comma
HeadersVariable = regexp.MustCompile(`^[A-Za-z0-9-_, ]*$`)
// URLWithNginxVariableRegex defines a url that can contain nginx variables.
// It is a risky operation
URLWithNginxVariableRegex = regexp.MustCompile("^[" + alphaNumericChars + urlEnabledChars + "$]*$")
)
// ValidateArrayOfServerName validates if all fields on a Server name annotation are
// regexes. They can be *.something*, ~^www\d+\.example\.com$ but not fancy character
func ValidateArrayOfServerName(value string) error {
for _, fqdn := range strings.Split(value, ",") {
if err := ValidateServerName(fqdn); err != nil {
return err
}
}
return nil
}
// ValidateServerName validates if the passed value is an acceptable server name. The server name
// can contain regex characters, as those are accepted values on nginx configuration
func ValidateServerName(value string) error {
value = strings.TrimSpace(value)
if !IsValidRegex.MatchString(value) {
return fmt.Errorf("value %s is invalid server name", value)
}
return nil
}
// ValidateRegex receives a regex as an argument and uses it to validate
// the value of the field.
// Annotation can define if the spaces should be trimmed before validating the value
func ValidateRegex(regex regexp.Regexp, removeSpace bool) AnnotationValidator {
return func(s string) error {
if removeSpace {
s = strings.ReplaceAll(s, " ", "")
}
if !regex.MatchString(s) {
return fmt.Errorf("value %s is invalid", s)
}
return nil
}
}
// ValidateOptions receives an array of valid options that can be the value of annotation.
// If no valid option is found, it will return an error
func ValidateOptions(options []string, caseSensitive bool, trimSpace bool) AnnotationValidator {
return func(s string) error {
if trimSpace {
s = strings.TrimSpace(s)
}
if !caseSensitive {
s = strings.ToLower(s)
}
for _, option := range options {
if s == option {
return nil
}
}
return fmt.Errorf("value does not match any valid option")
}
}
// ValidateBool validates if the specified value is a bool
func ValidateBool(value string) error {
_, err := strconv.ParseBool(value)
return err
}
// ValidateInt validates if the specified value is an integer
func ValidateInt(value string) error {
_, err := strconv.Atoi(value)
return err
}
// ValidateCIDRs validates if the specified value is an array of IPs and CIDRs
func ValidateCIDRs(value string) error {
_, err := net.ParseCIDRs(value)
return err
}
// ValidateDuration validates if the specified value is a valid time
func ValidateDuration(value string) error {
_, err := time.ParseDuration(value)
return err
}
// ValidateNull always return null values and should not be widely used.
// It is used on the "snippet" annotations, as it is up to the admin to allow its
// usage, knowing it can be critical!
func ValidateNull(value string) error {
return nil
}
// ValidateServiceName validates if a provided service name is a valid string
func ValidateServiceName(value string) error {
errs := machineryvalidation.NameIsDNS1035Label(value, false)
if len(errs) != 0 {
return fmt.Errorf("annotation does not contain a valid service name: %+v", errs)
}
return nil
}
// checkAnnotations will check each annotation for:
// 1 - Does it contain the internal validation and docs config?
// 2 - Does the ingress contains annotations? (validate null pointers)
// 3 - Does it contains a validator? Should it contain a validator (not containing is a bug!)
// 4 - Does the annotation contain aliases? So we should use if the alias is defined an the annotation not.
// 4 - Runs the validator on the value
// It will return the full annotation name if all is fine
func checkAnnotation(name string, ing *networking.Ingress, fields AnnotationFields) (string, error) {
var validateFunc AnnotationValidator
if fields != nil {
config, ok := fields[name]
if !ok {
return "", fmt.Errorf("annotation does not contain a valid internal configuration, this is an Ingress Controller issue! Please raise an issue on github.com/kubernetes/ingress-nginx")
}
validateFunc = config.Validator
}
if ing == nil || len(ing.GetAnnotations()) == 0 {
return "", ing_errors.ErrMissingAnnotations
}
annotationFullName := GetAnnotationWithPrefix(name)
if annotationFullName == "" {
return "", ing_errors.ErrInvalidAnnotationName
}
annotationValue := ing.GetAnnotations()[annotationFullName]
if fields != nil {
if validateFunc == nil {
return "", fmt.Errorf("annotation does not contain a validator. This is an ingress-controller bug. Please open an issue")
}
if annotationValue == "" {
for _, annotationAlias := range fields[name].AnnotationAliases {
tempAnnotationFullName := GetAnnotationWithPrefix(annotationAlias)
if aliasVal := ing.GetAnnotations()[tempAnnotationFullName]; aliasVal != "" {
annotationValue = aliasVal
annotationFullName = tempAnnotationFullName
break
}
}
}
// We don't run validation against empty values
if EnableAnnotationValidation && annotationValue != "" {
if err := validateFunc(annotationValue); err != nil {
klog.Warningf("validation error on ingress %s/%s: annotation %s contains invalid value %s", ing.GetNamespace(), ing.GetName(), name, annotationValue)
return "", ing_errors.NewValidationError(annotationFullName)
}
}
}
return annotationFullName, nil
}
func CheckAnnotationRisk(annotations map[string]string, maxrisk AnnotationRisk, config AnnotationFields) error {
var err error
for annotation := range annotations {
annPure := TrimAnnotationPrefix(annotation)
if cfg, ok := config[annPure]; ok && cfg.Risk > maxrisk {
err = errors.Join(err, fmt.Errorf("annotation %s is too risky for environment", annotation))
}
}
return err
}

View file

@ -0,0 +1,310 @@
/*
Copyright 2023 The Kubernetes Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package parser
import (
"fmt"
"testing"
networking "k8s.io/api/networking/v1"
v1 "k8s.io/apimachinery/pkg/apis/meta/v1"
)
func TestValidateArrayOfServerName(t *testing.T) {
tests := []struct {
name string
value string
wantErr bool
}{
{
name: "should accept common name",
value: "something.com,anything.com",
wantErr: false,
},
{
name: "should accept wildcard name",
value: "*.something.com,otherthing.com",
wantErr: false,
},
{
name: "should allow names with spaces between array and some regexes",
value: `~^www\d+\.example\.com$,something.com`,
wantErr: false,
},
{
name: "should allow names with regexes",
value: `http://some.test.env.com:2121/$someparam=1&$someotherparam=2`,
wantErr: false,
},
{
name: "should allow names with wildcard in middle common name",
value: "*.so*mething.com,bla.com",
wantErr: false,
},
{
name: "should deny names with weird characters",
value: "something.com,lolo;xpto.com,nothing.com",
wantErr: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if err := ValidateArrayOfServerName(tt.value); (err != nil) != tt.wantErr {
t.Errorf("ValidateArrayOfServerName() error = %v, wantErr %v", err, tt.wantErr)
}
})
}
}
func Test_checkAnnotation(t *testing.T) {
type args struct {
name string
ing *networking.Ingress
fields AnnotationFields
}
tests := []struct {
name string
args args
want string
wantErr bool
}{
{
name: "null ingress should error",
want: "",
args: args{
name: "some-random-annotation",
},
wantErr: true,
},
{
name: "not having a validator for a specific annotation is a bug",
want: "",
args: args{
name: "some-new-invalid-annotation",
ing: &networking.Ingress{
ObjectMeta: v1.ObjectMeta{
Annotations: map[string]string{
GetAnnotationWithPrefix("some-new-invalid-annotation"): "xpto",
},
},
},
fields: AnnotationFields{
"otherannotation": AnnotationConfig{
Validator: func(value string) error { return nil },
},
},
},
wantErr: true,
},
{
name: "annotationconfig found and no validation func defined on annotation is a bug",
want: "",
args: args{
name: "some-new-invalid-annotation",
ing: &networking.Ingress{
ObjectMeta: v1.ObjectMeta{
Annotations: map[string]string{
GetAnnotationWithPrefix("some-new-invalid-annotation"): "xpto",
},
},
},
fields: AnnotationFields{
"some-new-invalid-annotation": AnnotationConfig{},
},
},
wantErr: true,
},
{
name: "no annotation can turn into a null pointer and should fail",
want: "",
args: args{
name: "some-new-invalid-annotation",
ing: &networking.Ingress{
ObjectMeta: v1.ObjectMeta{},
},
fields: AnnotationFields{
"some-new-invalid-annotation": AnnotationConfig{},
},
},
wantErr: true,
},
{
name: "no AnnotationField config should bypass validations",
want: GetAnnotationWithPrefix("some-valid-annotation"),
args: args{
name: "some-valid-annotation",
ing: &networking.Ingress{
ObjectMeta: v1.ObjectMeta{
Annotations: map[string]string{
GetAnnotationWithPrefix("some-valid-annotation"): "xpto",
},
},
},
},
wantErr: false,
},
{
name: "annotation with invalid value should fail",
want: "",
args: args{
name: "some-new-annotation",
ing: &networking.Ingress{
ObjectMeta: v1.ObjectMeta{
Annotations: map[string]string{
GetAnnotationWithPrefix("some-new-annotation"): "xpto1",
},
},
},
fields: AnnotationFields{
"some-new-annotation": AnnotationConfig{
Validator: func(value string) error {
if value != "xpto" {
return fmt.Errorf("this is an error")
}
return nil
},
},
},
},
wantErr: true,
},
{
name: "annotation with valid value should pass",
want: GetAnnotationWithPrefix("some-other-annotation"),
args: args{
name: "some-other-annotation",
ing: &networking.Ingress{
ObjectMeta: v1.ObjectMeta{
Annotations: map[string]string{
GetAnnotationWithPrefix("some-other-annotation"): "xpto",
},
},
},
fields: AnnotationFields{
"some-other-annotation": AnnotationConfig{
Validator: func(value string) error {
if value != "xpto" {
return fmt.Errorf("this is an error")
}
return nil
},
},
},
},
wantErr: false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got, err := checkAnnotation(tt.args.name, tt.args.ing, tt.args.fields)
if (err != nil) != tt.wantErr {
t.Errorf("checkAnnotation() error = %v, wantErr %v", err, tt.wantErr)
return
}
if got != tt.want {
t.Errorf("checkAnnotation() = %v, want %v", got, tt.want)
}
})
}
}
func TestCheckAnnotationRisk(t *testing.T) {
tests := []struct {
name string
annotations map[string]string
maxrisk AnnotationRisk
config AnnotationFields
wantErr bool
}{
{
name: "high risk should not be accepted with maximum medium",
maxrisk: AnnotationRiskMedium,
annotations: map[string]string{
"nginx.ingress.kubernetes.io/bla": "blo",
"nginx.ingress.kubernetes.io/bli": "bl3",
},
config: AnnotationFields{
"bla": {
Risk: AnnotationRiskHigh,
},
"bli": {
Risk: AnnotationRiskMedium,
},
},
wantErr: true,
},
{
name: "high risk should be accepted with maximum critical",
maxrisk: AnnotationRiskCritical,
annotations: map[string]string{
"nginx.ingress.kubernetes.io/bla": "blo",
"nginx.ingress.kubernetes.io/bli": "bl3",
},
config: AnnotationFields{
"bla": {
Risk: AnnotationRiskHigh,
},
"bli": {
Risk: AnnotationRiskMedium,
},
},
wantErr: false,
},
{
name: "low risk should be accepted with maximum low",
maxrisk: AnnotationRiskLow,
annotations: map[string]string{
"nginx.ingress.kubernetes.io/bla": "blo",
"nginx.ingress.kubernetes.io/bli": "bl3",
},
config: AnnotationFields{
"bla": {
Risk: AnnotationRiskLow,
},
"bli": {
Risk: AnnotationRiskLow,
},
},
wantErr: false,
},
{
name: "critical risk should be accepted with maximum critical",
maxrisk: AnnotationRiskCritical,
annotations: map[string]string{
"nginx.ingress.kubernetes.io/bla": "blo",
"nginx.ingress.kubernetes.io/bli": "bl3",
},
config: AnnotationFields{
"bla": {
Risk: AnnotationRiskCritical,
},
"bli": {
Risk: AnnotationRiskCritical,
},
},
wantErr: false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if err := CheckAnnotationRisk(tt.annotations, tt.maxrisk, tt.config); (err != nil) != tt.wantErr {
t.Errorf("CheckAnnotationRisk() error = %v, wantErr %v", err, tt.wantErr)
}
})
}
}

View file

@ -23,22 +23,51 @@ import (
"k8s.io/ingress-nginx/internal/ingress/resolver"
)
const (
portsInRedirectAnnotation = "use-port-in-redirects"
)
var portsInRedirectAnnotations = parser.Annotation{
Group: "redirect",
Annotations: parser.AnnotationFields{
portsInRedirectAnnotation: {
Validator: parser.ValidateBool,
Scope: parser.AnnotationScopeLocation,
Risk: parser.AnnotationRiskLow, // Low, as it allows just a set of options
Documentation: `Enables or disables specifying the port in absolute redirects issued by nginx.`,
},
},
}
type portInRedirect struct {
r resolver.Resolver
r resolver.Resolver
annotationConfig parser.Annotation
}
// NewParser creates a new port in redirect annotation parser
func NewParser(r resolver.Resolver) parser.IngressAnnotation {
return portInRedirect{r}
return portInRedirect{
r: r,
annotationConfig: portsInRedirectAnnotations,
}
}
// Parse parses the annotations contained in the ingress
// rule used to indicate if the redirects must
func (a portInRedirect) Parse(ing *networking.Ingress) (interface{}, error) {
up, err := parser.GetBoolAnnotation("use-port-in-redirects", ing)
up, err := parser.GetBoolAnnotation(portsInRedirectAnnotation, ing, a.annotationConfig.Annotations)
if err != nil {
return a.r.GetDefaultBackend().UsePortInRedirects, nil
}
return up, nil
}
func (a portInRedirect) GetDocumentation() parser.AnnotationFields {
return a.annotationConfig.Annotations
}
func (a portInRedirect) Validate(anns map[string]string) error {
maxrisk := parser.StringRiskToRisk(a.r.GetSecurityConfiguration().AnnotationsRiskLevel)
return parser.CheckAnnotationRisk(anns, maxrisk, portsInRedirectAnnotations.Annotations)
}

View file

@ -17,7 +17,6 @@ limitations under the License.
package portinredirect
import (
"fmt"
"testing"
api "k8s.io/api/core/v1"
@ -84,23 +83,24 @@ func (m mockBackend) GetDefaultBackend() defaults.Backend {
func TestPortInRedirect(t *testing.T) {
tests := []struct {
title string
usePort *bool
usePort string
def bool
exp bool
}{
{"false - default false", newFalse(), false, false},
{"false - default true", newFalse(), true, false},
{"no annotation - default false", nil, false, false},
{"no annotation - default true", nil, true, true},
{"true - default true", newTrue(), true, true},
{"false - default false", "false", false, false},
{"false - default true", "false", true, false},
{"no annotation - default false", "", false, false},
{"no annotation - default false", "not-a-bool", false, false},
{"no annotation - default true", "", true, true},
{"true - default true", "true", true, true},
}
for _, test := range tests {
ing := buildIngress()
data := map[string]string{}
if test.usePort != nil {
data[parser.GetAnnotationWithPrefix("use-port-in-redirects")] = fmt.Sprintf("%v", *test.usePort)
if test.usePort != "" {
data[parser.GetAnnotationWithPrefix(portsInRedirectAnnotation)] = test.usePort
}
ing.SetAnnotations(data)
@ -118,13 +118,3 @@ func TestPortInRedirect(t *testing.T) {
}
}
}
func newTrue() *bool {
b := true
return &b
}
func newFalse() *bool {
b := false
return &b
}

View file

@ -17,12 +17,150 @@ limitations under the License.
package proxy
import (
"regexp"
networking "k8s.io/api/networking/v1"
"k8s.io/ingress-nginx/internal/ingress/annotations/parser"
"k8s.io/ingress-nginx/internal/ingress/resolver"
)
const (
proxyConnectTimeoutAnnotation = "proxy-connect-timeout"
proxySendTimeoutAnnotation = "proxy-send-timeout"
proxyReadTimeoutAnnotation = "proxy-read-timeout"
proxyBuffersNumberAnnotation = "proxy-buffers-number"
proxyBufferSizeAnnotation = "proxy-buffer-size"
proxyCookiePathAnnotation = "proxy-cookie-path"
proxyCookieDomainAnnotation = "proxy-cookie-domain"
proxyBodySizeAnnotation = "proxy-body-size"
proxyNextUpstreamAnnotation = "proxy-next-upstream"
proxyNextUpstreamTimeoutAnnotation = "proxy-next-upstream-timeout"
proxyNextUpstreamTriesAnnotation = "proxy-next-upstream-tries"
proxyRequestBufferingAnnotation = "proxy-request-buffering"
proxyRedirectFromAnnotation = "proxy-redirect-from"
proxyRedirectToAnnotation = "proxy-redirect-to"
proxyBufferingAnnotation = "proxy-buffering"
proxyHTTPVersionAnnotation = "proxy-http-version"
proxyMaxTempFileSizeAnnotation = "proxy-max-temp-file-size"
)
var (
validUpstreamAnnotation = regexp.MustCompile(`^((error|timeout|invalid_header|http_500|http_502|http_503|http_504|http_403|http_404|http_429|non_idempotent|off)\s?)+$`)
)
var proxyAnnotations = parser.Annotation{
Group: "backend",
Annotations: parser.AnnotationFields{
proxyConnectTimeoutAnnotation: {
Validator: parser.ValidateInt,
Scope: parser.AnnotationScopeLocation,
Risk: parser.AnnotationRiskLow,
Documentation: `This annotation allows setting the timeout in seconds of the connect operation to the backend.`,
},
proxySendTimeoutAnnotation: {
Validator: parser.ValidateInt,
Scope: parser.AnnotationScopeLocation,
Risk: parser.AnnotationRiskLow,
Documentation: `This annotation allows setting the timeout in seconds of the send operation to the backend.`,
},
proxyReadTimeoutAnnotation: {
Validator: parser.ValidateInt,
Scope: parser.AnnotationScopeLocation,
Risk: parser.AnnotationRiskLow,
Documentation: `This annotation allows setting the timeout in seconds of the read operation to the backend.`,
},
proxyBuffersNumberAnnotation: {
Validator: parser.ValidateInt,
Scope: parser.AnnotationScopeLocation,
Risk: parser.AnnotationRiskLow,
Documentation: `This annotation sets the number of the buffers in proxy_buffers used for reading the first part of the response received from the proxied server.
By default proxy buffers number is set as 4`,
},
proxyBufferSizeAnnotation: {
Validator: parser.ValidateRegex(*parser.SizeRegex, true),
Scope: parser.AnnotationScopeLocation,
Risk: parser.AnnotationRiskLow,
Documentation: `This annotation sets the size of the buffer proxy_buffer_size used for reading the first part of the response received from the proxied server.
By default proxy buffer size is set as "4k".`,
},
proxyCookiePathAnnotation: {
Validator: parser.ValidateRegex(*parser.URLIsValidRegex, true),
Scope: parser.AnnotationScopeLocation,
Risk: parser.AnnotationRiskMedium,
Documentation: `This annotation sets a text that should be changed in the path attribute of the "Set-Cookie" header fields of a proxied server response.`,
},
proxyCookieDomainAnnotation: {
Validator: parser.ValidateRegex(*parser.BasicCharsRegex, true),
Scope: parser.AnnotationScopeLocation,
Risk: parser.AnnotationRiskMedium,
Documentation: `This annotation ets a text that should be changed in the domain attribute of the "Set-Cookie" header fields of a proxied server response.`,
},
proxyBodySizeAnnotation: {
Validator: parser.ValidateRegex(*parser.SizeRegex, true),
Scope: parser.AnnotationScopeLocation,
Risk: parser.AnnotationRiskMedium,
Documentation: `This annotation allows setting the maximum allowed size of a client request body.`,
},
proxyNextUpstreamAnnotation: {
Validator: parser.ValidateRegex(*validUpstreamAnnotation, false),
Scope: parser.AnnotationScopeLocation,
Risk: parser.AnnotationRiskMedium,
Documentation: `This annotation defines when the next upstream should be used.
This annotation reflect the directive https://nginx.org/en/docs/http/ngx_http_proxy_module.html#proxy_next_upstream
and only the allowed values on upstream are allowed here.`,
},
proxyNextUpstreamTimeoutAnnotation: {
Validator: parser.ValidateInt,
Scope: parser.AnnotationScopeLocation,
Risk: parser.AnnotationRiskLow,
Documentation: `This annotation limits the time during which a request can be passed to the next server`,
},
proxyNextUpstreamTriesAnnotation: {
Validator: parser.ValidateInt,
Scope: parser.AnnotationScopeLocation,
Risk: parser.AnnotationRiskLow,
Documentation: `This annotation limits the number of possible tries for passing a request to the next server`,
},
proxyRequestBufferingAnnotation: {
Validator: parser.ValidateOptions([]string{"on", "off"}, true, true),
Scope: parser.AnnotationScopeLocation,
Risk: parser.AnnotationRiskLow,
Documentation: `This annotation enables or disables buffering of a client request body.`,
},
proxyRedirectFromAnnotation: {
Validator: parser.ValidateRegex(*parser.URLIsValidRegex, true),
Scope: parser.AnnotationScopeLocation,
Risk: parser.AnnotationRiskMedium,
Documentation: `The annotations proxy-redirect-from and proxy-redirect-to will set the first and second parameters of NGINX's proxy_redirect directive respectively`,
},
proxyRedirectToAnnotation: {
Validator: parser.ValidateRegex(*parser.URLIsValidRegex, true),
Scope: parser.AnnotationScopeLocation,
Risk: parser.AnnotationRiskMedium,
Documentation: `The annotations proxy-redirect-from and proxy-redirect-to will set the first and second parameters of NGINX's proxy_redirect directive respectively`,
},
proxyBufferingAnnotation: {
Validator: parser.ValidateOptions([]string{"on", "off"}, true, true),
Scope: parser.AnnotationScopeLocation,
Risk: parser.AnnotationRiskLow,
Documentation: `This annotation enables or disables buffering of responses from the proxied server. It can be "on" or "off"`,
},
proxyHTTPVersionAnnotation: {
Validator: parser.ValidateOptions([]string{"1.0", "1.1"}, true, true),
Scope: parser.AnnotationScopeLocation,
Risk: parser.AnnotationRiskLow,
Documentation: `This annotations sets the HTTP protocol version for proxying. Can be "1.0" or "1.1".`,
},
proxyMaxTempFileSizeAnnotation: {
Validator: parser.ValidateRegex(*parser.SizeRegex, true),
Scope: parser.AnnotationScopeLocation,
Risk: parser.AnnotationRiskLow,
Documentation: `This annotation defines the maximum size of a temporary file when buffering responses.`,
},
},
}
// Config returns the proxy timeout to use in the upstream server/s
type Config struct {
BodySize string `json:"bodySize"`
@ -109,12 +247,15 @@ func (l1 *Config) Equal(l2 *Config) bool {
}
type proxy struct {
r resolver.Resolver
r resolver.Resolver
annotationConfig parser.Annotation
}
// NewParser creates a new reverse proxy configuration annotation parser
func NewParser(r resolver.Resolver) parser.IngressAnnotation {
return proxy{r}
return proxy{r: r,
annotationConfig: proxyAnnotations,
}
}
// ParseAnnotations parses the annotations contained in the ingress
@ -125,90 +266,99 @@ func (a proxy) Parse(ing *networking.Ingress) (interface{}, error) {
var err error
config.ConnectTimeout, err = parser.GetIntAnnotation("proxy-connect-timeout", ing)
config.ConnectTimeout, err = parser.GetIntAnnotation(proxyConnectTimeoutAnnotation, ing, a.annotationConfig.Annotations)
if err != nil {
config.ConnectTimeout = defBackend.ProxyConnectTimeout
}
config.SendTimeout, err = parser.GetIntAnnotation("proxy-send-timeout", ing)
config.SendTimeout, err = parser.GetIntAnnotation(proxySendTimeoutAnnotation, ing, a.annotationConfig.Annotations)
if err != nil {
config.SendTimeout = defBackend.ProxySendTimeout
}
config.ReadTimeout, err = parser.GetIntAnnotation("proxy-read-timeout", ing)
config.ReadTimeout, err = parser.GetIntAnnotation(proxyReadTimeoutAnnotation, ing, a.annotationConfig.Annotations)
if err != nil {
config.ReadTimeout = defBackend.ProxyReadTimeout
}
config.BuffersNumber, err = parser.GetIntAnnotation("proxy-buffers-number", ing)
config.BuffersNumber, err = parser.GetIntAnnotation(proxyBuffersNumberAnnotation, ing, a.annotationConfig.Annotations)
if err != nil {
config.BuffersNumber = defBackend.ProxyBuffersNumber
}
config.BufferSize, err = parser.GetStringAnnotation("proxy-buffer-size", ing)
config.BufferSize, err = parser.GetStringAnnotation(proxyBufferSizeAnnotation, ing, a.annotationConfig.Annotations)
if err != nil {
config.BufferSize = defBackend.ProxyBufferSize
}
config.CookiePath, err = parser.GetStringAnnotation("proxy-cookie-path", ing)
config.CookiePath, err = parser.GetStringAnnotation(proxyCookiePathAnnotation, ing, a.annotationConfig.Annotations)
if err != nil {
config.CookiePath = defBackend.ProxyCookiePath
}
config.CookieDomain, err = parser.GetStringAnnotation("proxy-cookie-domain", ing)
config.CookieDomain, err = parser.GetStringAnnotation(proxyCookieDomainAnnotation, ing, a.annotationConfig.Annotations)
if err != nil {
config.CookieDomain = defBackend.ProxyCookieDomain
}
config.BodySize, err = parser.GetStringAnnotation("proxy-body-size", ing)
config.BodySize, err = parser.GetStringAnnotation(proxyBodySizeAnnotation, ing, a.annotationConfig.Annotations)
if err != nil {
config.BodySize = defBackend.ProxyBodySize
}
config.NextUpstream, err = parser.GetStringAnnotation("proxy-next-upstream", ing)
config.NextUpstream, err = parser.GetStringAnnotation(proxyNextUpstreamAnnotation, ing, a.annotationConfig.Annotations)
if err != nil {
config.NextUpstream = defBackend.ProxyNextUpstream
}
config.NextUpstreamTimeout, err = parser.GetIntAnnotation("proxy-next-upstream-timeout", ing)
config.NextUpstreamTimeout, err = parser.GetIntAnnotation(proxyNextUpstreamTimeoutAnnotation, ing, a.annotationConfig.Annotations)
if err != nil {
config.NextUpstreamTimeout = defBackend.ProxyNextUpstreamTimeout
}
config.NextUpstreamTries, err = parser.GetIntAnnotation("proxy-next-upstream-tries", ing)
config.NextUpstreamTries, err = parser.GetIntAnnotation(proxyNextUpstreamTriesAnnotation, ing, a.annotationConfig.Annotations)
if err != nil {
config.NextUpstreamTries = defBackend.ProxyNextUpstreamTries
}
config.RequestBuffering, err = parser.GetStringAnnotation("proxy-request-buffering", ing)
config.RequestBuffering, err = parser.GetStringAnnotation(proxyRequestBufferingAnnotation, ing, a.annotationConfig.Annotations)
if err != nil {
config.RequestBuffering = defBackend.ProxyRequestBuffering
}
config.ProxyRedirectFrom, err = parser.GetStringAnnotation("proxy-redirect-from", ing)
config.ProxyRedirectFrom, err = parser.GetStringAnnotation(proxyRedirectFromAnnotation, ing, a.annotationConfig.Annotations)
if err != nil {
config.ProxyRedirectFrom = defBackend.ProxyRedirectFrom
}
config.ProxyRedirectTo, err = parser.GetStringAnnotation("proxy-redirect-to", ing)
config.ProxyRedirectTo, err = parser.GetStringAnnotation(proxyRedirectToAnnotation, ing, a.annotationConfig.Annotations)
if err != nil {
config.ProxyRedirectTo = defBackend.ProxyRedirectTo
}
config.ProxyBuffering, err = parser.GetStringAnnotation("proxy-buffering", ing)
config.ProxyBuffering, err = parser.GetStringAnnotation(proxyBufferingAnnotation, ing, a.annotationConfig.Annotations)
if err != nil {
config.ProxyBuffering = defBackend.ProxyBuffering
}
config.ProxyHTTPVersion, err = parser.GetStringAnnotation("proxy-http-version", ing)
config.ProxyHTTPVersion, err = parser.GetStringAnnotation(proxyHTTPVersionAnnotation, ing, a.annotationConfig.Annotations)
if err != nil {
config.ProxyHTTPVersion = defBackend.ProxyHTTPVersion
}
config.ProxyMaxTempFileSize, err = parser.GetStringAnnotation("proxy-max-temp-file-size", ing)
config.ProxyMaxTempFileSize, err = parser.GetStringAnnotation(proxyMaxTempFileSizeAnnotation, ing, a.annotationConfig.Annotations)
if err != nil {
config.ProxyMaxTempFileSize = defBackend.ProxyMaxTempFileSize
}
return config, nil
}
func (a proxy) GetDocumentation() parser.AnnotationFields {
return a.annotationConfig.Annotations
}
func (a proxy) Validate(anns map[string]string) error {
maxrisk := parser.StringRiskToRisk(a.r.GetSecurityConfiguration().AnnotationsRiskLevel)
return parser.CheckAnnotationRisk(anns, maxrisk, proxyAnnotations.Annotations)
}

View file

@ -161,6 +161,74 @@ func TestProxy(t *testing.T) {
}
}
func TestProxyComplex(t *testing.T) {
ing := buildIngress()
data := map[string]string{}
data[parser.GetAnnotationWithPrefix("proxy-connect-timeout")] = "1"
data[parser.GetAnnotationWithPrefix("proxy-send-timeout")] = "2"
data[parser.GetAnnotationWithPrefix("proxy-read-timeout")] = "3"
data[parser.GetAnnotationWithPrefix("proxy-buffers-number")] = "8"
data[parser.GetAnnotationWithPrefix("proxy-buffer-size")] = "1k"
data[parser.GetAnnotationWithPrefix("proxy-body-size")] = "2k"
data[parser.GetAnnotationWithPrefix("proxy-next-upstream")] = "error http_502"
data[parser.GetAnnotationWithPrefix("proxy-next-upstream-timeout")] = "5"
data[parser.GetAnnotationWithPrefix("proxy-next-upstream-tries")] = "3"
data[parser.GetAnnotationWithPrefix("proxy-request-buffering")] = "off"
data[parser.GetAnnotationWithPrefix("proxy-buffering")] = "on"
data[parser.GetAnnotationWithPrefix("proxy-http-version")] = "1.0"
data[parser.GetAnnotationWithPrefix("proxy-max-temp-file-size")] = "128k"
ing.SetAnnotations(data)
i, err := NewParser(mockBackend{}).Parse(ing)
if err != nil {
t.Fatalf("unexpected error parsing a valid")
}
p, ok := i.(*Config)
if !ok {
t.Fatalf("expected a Config type")
}
if p.ConnectTimeout != 1 {
t.Errorf("expected 1 as connect-timeout but returned %v", p.ConnectTimeout)
}
if p.SendTimeout != 2 {
t.Errorf("expected 2 as send-timeout but returned %v", p.SendTimeout)
}
if p.ReadTimeout != 3 {
t.Errorf("expected 3 as read-timeout but returned %v", p.ReadTimeout)
}
if p.BuffersNumber != 8 {
t.Errorf("expected 8 as proxy-buffers-number but returned %v", p.BuffersNumber)
}
if p.BufferSize != "1k" {
t.Errorf("expected 1k as buffer-size but returned %v", p.BufferSize)
}
if p.BodySize != "2k" {
t.Errorf("expected 2k as body-size but returned %v", p.BodySize)
}
if p.NextUpstream != "error http_502" {
t.Errorf("expected off as next-upstream but returned %v", p.NextUpstream)
}
if p.NextUpstreamTimeout != 5 {
t.Errorf("expected 5 as next-upstream-timeout but returned %v", p.NextUpstreamTimeout)
}
if p.NextUpstreamTries != 3 {
t.Errorf("expected 3 as next-upstream-tries but returned %v", p.NextUpstreamTries)
}
if p.RequestBuffering != "off" {
t.Errorf("expected off as request-buffering but returned %v", p.RequestBuffering)
}
if p.ProxyBuffering != "on" {
t.Errorf("expected on as proxy-buffering but returned %v", p.ProxyBuffering)
}
if p.ProxyHTTPVersion != "1.0" {
t.Errorf("expected 1.0 as proxy-http-version but returned %v", p.ProxyHTTPVersion)
}
if p.ProxyMaxTempFileSize != "128k" {
t.Errorf("expected 128k as proxy-max-temp-file-size but returned %v", p.ProxyMaxTempFileSize)
}
}
func TestProxyWithNoAnnotation(t *testing.T) {
ing := buildIngress()

View file

@ -24,9 +24,11 @@ import (
networking "k8s.io/api/networking/v1"
"k8s.io/ingress-nginx/internal/ingress/annotations/parser"
"k8s.io/ingress-nginx/internal/ingress/errors"
ing_errors "k8s.io/ingress-nginx/internal/ingress/errors"
"k8s.io/ingress-nginx/internal/ingress/resolver"
"k8s.io/ingress-nginx/internal/k8s"
"k8s.io/klog/v2"
)
const (
@ -39,9 +41,73 @@ const (
var (
proxySSLOnOffRegex = regexp.MustCompile(`^(on|off)$`)
proxySSLProtocolRegex = regexp.MustCompile(`^(SSLv2|SSLv3|TLSv1|TLSv1\.1|TLSv1\.2|TLSv1\.3)$`)
proxySSLProtocolRegex = regexp.MustCompile(`^(SSLv2|SSLv3|TLSv1|TLSv1\.1|TLSv1\.2|TLSv1\.3| )*$`)
proxySSLCiphersRegex = regexp.MustCompile(`^[A-Za-z0-9\+\:\_\-\!]*$`)
)
const (
proxySSLSecretAnnotation = "proxy-ssl-secret"
proxySSLCiphersAnnotation = "proxy-ssl-ciphers"
proxySSLProtocolsAnnotation = "proxy-ssl-protocols"
proxySSLNameAnnotation = "proxy-ssl-name"
proxySSLVerifyAnnotation = "proxy-ssl-verify"
proxySSLVerifyDepthAnnotation = "proxy-ssl-verify-depth"
proxySSLServerNameAnnotation = "proxy-ssl-server-name"
)
var proxySSLAnnotation = parser.Annotation{
Group: "proxy",
Annotations: parser.AnnotationFields{
proxySSLSecretAnnotation: {
Validator: parser.ValidateRegex(*parser.BasicCharsRegex, true),
Scope: parser.AnnotationScopeIngress,
Risk: parser.AnnotationRiskMedium,
Documentation: `This annotation specifies a Secret with the certificate tls.crt, key tls.key in PEM format used for authentication to a proxied HTTPS server.
It should also contain trusted CA certificates ca.crt in PEM format used to verify the certificate of the proxied HTTPS server.
This annotation expects the Secret name in the form "namespace/secretName"
Just secrets on the same namespace of the ingress can be used.`,
},
proxySSLCiphersAnnotation: {
Validator: parser.ValidateRegex(*proxySSLCiphersRegex, true),
Scope: parser.AnnotationScopeIngress,
Risk: parser.AnnotationRiskMedium,
Documentation: `This annotation Specifies the enabled ciphers for requests to a proxied HTTPS server.
The ciphers are specified in the format understood by the OpenSSL library.`,
},
proxySSLProtocolsAnnotation: {
Validator: parser.ValidateRegex(*proxySSLProtocolRegex, true),
Scope: parser.AnnotationScopeIngress,
Risk: parser.AnnotationRiskLow,
Documentation: `This annotation enables the specified protocols for requests to a proxied HTTPS server.`,
},
proxySSLNameAnnotation: {
Validator: parser.ValidateServerName,
Scope: parser.AnnotationScopeIngress,
Risk: parser.AnnotationRiskHigh,
Documentation: `This annotation allows to set proxy_ssl_name. This allows overriding the server name used to verify the certificate of the proxied HTTPS server.
This value is also passed through SNI when a connection is established to the proxied HTTPS server.`,
},
proxySSLVerifyAnnotation: {
Validator: parser.ValidateRegex(*proxySSLOnOffRegex, true),
Scope: parser.AnnotationScopeIngress,
Risk: parser.AnnotationRiskLow,
Documentation: `This annotation enables or disables verification of the proxied HTTPS server certificate. (default: off)`,
},
proxySSLVerifyDepthAnnotation: {
Validator: parser.ValidateInt,
Scope: parser.AnnotationScopeIngress,
Risk: parser.AnnotationRiskLow,
Documentation: `This annotation Sets the verification depth in the proxied HTTPS server certificates chain. (default: 1).`,
},
proxySSLServerNameAnnotation: {
Validator: parser.ValidateRegex(*proxySSLOnOffRegex, true),
Scope: parser.AnnotationScopeIngress,
Risk: parser.AnnotationRiskLow,
Documentation: `This annotation enables passing of the server name through TLS Server Name Indication extension (SNI, RFC 6066) when establishing a connection with the proxied HTTPS server.`,
},
},
}
// Config contains the AuthSSLCert used for mutual authentication
// and the configured VerifyDepth
type Config struct {
@ -85,11 +151,14 @@ func (pssl1 *Config) Equal(pssl2 *Config) bool {
// NewParser creates a new TLS authentication annotation parser
func NewParser(resolver resolver.Resolver) parser.IngressAnnotation {
return proxySSL{resolver}
return proxySSL{
r: resolver,
annotationConfig: proxySSLAnnotation}
}
type proxySSL struct {
r resolver.Resolver
r resolver.Resolver
annotationConfig parser.Annotation
}
func sortProtocols(protocols string) string {
@ -120,16 +189,22 @@ func (p proxySSL) Parse(ing *networking.Ingress) (interface{}, error) {
var err error
config := &Config{}
proxysslsecret, err := parser.GetStringAnnotation("proxy-ssl-secret", ing)
proxysslsecret, err := parser.GetStringAnnotation(proxySSLSecretAnnotation, ing, p.annotationConfig.Annotations)
if err != nil {
return &Config{}, err
}
_, _, err = k8s.ParseNameNS(proxysslsecret)
ns, _, err := k8s.ParseNameNS(proxysslsecret)
if err != nil {
return &Config{}, ing_errors.NewLocationDenied(err.Error())
}
secCfg := p.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")
}
proxyCert, err := p.r.GetAuthCertificate(proxysslsecret)
if err != nil {
e := fmt.Errorf("error obtaining certificate: %w", err)
@ -137,37 +212,55 @@ func (p proxySSL) Parse(ing *networking.Ingress) (interface{}, error) {
}
config.AuthSSLCert = *proxyCert
config.Ciphers, err = parser.GetStringAnnotation("proxy-ssl-ciphers", ing)
config.Ciphers, err = parser.GetStringAnnotation(proxySSLCiphersAnnotation, ing, p.annotationConfig.Annotations)
if err != nil {
if errors.IsValidationError(err) {
klog.Warningf("invalid value passed to proxy-ssl-ciphers, defaulting to %s", defaultProxySSLCiphers)
}
config.Ciphers = defaultProxySSLCiphers
}
config.Protocols, err = parser.GetStringAnnotation("proxy-ssl-protocols", ing)
config.Protocols, err = parser.GetStringAnnotation(proxySSLProtocolsAnnotation, ing, p.annotationConfig.Annotations)
if err != nil {
if errors.IsValidationError(err) {
klog.Warningf("invalid value passed to proxy-ssl-protocols, defaulting to %s", defaultProxySSLProtocols)
}
config.Protocols = defaultProxySSLProtocols
} else {
config.Protocols = sortProtocols(config.Protocols)
}
config.ProxySSLName, err = parser.GetStringAnnotation("proxy-ssl-name", ing)
config.ProxySSLName, err = parser.GetStringAnnotation(proxySSLNameAnnotation, ing, p.annotationConfig.Annotations)
if err != nil {
if errors.IsValidationError(err) {
klog.Warningf("invalid value passed to proxy-ssl-name, defaulting to empty")
}
config.ProxySSLName = ""
}
config.Verify, err = parser.GetStringAnnotation("proxy-ssl-verify", ing)
config.Verify, err = parser.GetStringAnnotation(proxySSLVerifyAnnotation, ing, p.annotationConfig.Annotations)
if err != nil || !proxySSLOnOffRegex.MatchString(config.Verify) {
config.Verify = defaultProxySSLVerify
}
config.VerifyDepth, err = parser.GetIntAnnotation("proxy-ssl-verify-depth", ing)
config.VerifyDepth, err = parser.GetIntAnnotation(proxySSLVerifyDepthAnnotation, ing, p.annotationConfig.Annotations)
if err != nil || config.VerifyDepth == 0 {
config.VerifyDepth = defaultProxySSLVerifyDepth
}
config.ProxySSLServerName, err = parser.GetStringAnnotation("proxy-ssl-server-name", ing)
config.ProxySSLServerName, err = parser.GetStringAnnotation(proxySSLServerNameAnnotation, ing, p.annotationConfig.Annotations)
if err != nil || !proxySSLOnOffRegex.MatchString(config.ProxySSLServerName) {
config.ProxySSLServerName = defaultProxySSLServerName
}
return config, nil
}
func (p proxySSL) GetDocumentation() parser.AnnotationFields {
return p.annotationConfig.Annotations
}
func (a proxySSL) Validate(anns map[string]string) error {
maxrisk := parser.StringRiskToRisk(a.r.GetSecurityConfiguration().AnnotationsRiskLevel)
return parser.CheckAnnotationRisk(anns, maxrisk, proxySSLAnnotation.Annotations)
}

View file

@ -93,7 +93,7 @@ func TestAnnotations(t *testing.T) {
ing := buildIngress()
data := map[string]string{}
data[parser.GetAnnotationWithPrefix("proxy-ssl-secret")] = "default/demo-secret"
data[parser.GetAnnotationWithPrefix(proxySSLSecretAnnotation)] = "default/demo-secret"
data[parser.GetAnnotationWithPrefix("proxy-ssl-ciphers")] = "HIGH:-SHA"
data[parser.GetAnnotationWithPrefix("proxy-ssl-name")] = "$host"
data[parser.GetAnnotationWithPrefix("proxy-ssl-protocols")] = "TLSv1.3 SSLv2 TLSv1 TLSv1.2"

View file

@ -24,6 +24,7 @@ import (
networking "k8s.io/api/networking/v1"
"k8s.io/ingress-nginx/internal/ingress/annotations/parser"
"k8s.io/ingress-nginx/internal/ingress/errors"
"k8s.io/ingress-nginx/internal/ingress/resolver"
"k8s.io/ingress-nginx/internal/net"
"k8s.io/ingress-nginx/pkg/util/sets"
@ -58,7 +59,7 @@ type Config struct {
ID string `json:"id"`
Whitelist []string `json:"whitelist"`
Allowlist []string `json:"allowlist"`
}
// Equal tests for equality between two RateLimit types
@ -90,11 +91,11 @@ func (rt1 *Config) Equal(rt2 *Config) bool {
if rt1.Name != rt2.Name {
return false
}
if len(rt1.Whitelist) != len(rt2.Whitelist) {
if len(rt1.Allowlist) != len(rt2.Allowlist) {
return false
}
return sets.StringElementsMatch(rt1.Whitelist, rt2.Whitelist)
return sets.StringElementsMatch(rt1.Allowlist, rt2.Allowlist)
}
// Zone returns information about the NGINX rate limit (limit_req_zone)
@ -131,43 +132,121 @@ func (z1 *Zone) Equal(z2 *Zone) bool {
return true
}
const (
limitRateAnnotation = "limit-rate"
limitRateAfterAnnotation = "limit-rate-after"
limitRateRPMAnnotation = "limit-rpm"
limitRateRPSAnnotation = "limit-rps"
limitRateConnectionsAnnotation = "limit-connections"
limitRateBurstMultiplierAnnotation = "limit-burst-multiplier"
limitWhitelistAnnotation = "limit-whitelist" // This annotation is an alias for limit-allowlist
limitAllowlistAnnotation = "limit-allowlist"
)
var rateLimitAnnotations = parser.Annotation{
Group: "rate-limit",
Annotations: parser.AnnotationFields{
limitRateAnnotation: {
Validator: parser.ValidateInt,
Scope: parser.AnnotationScopeLocation,
Risk: parser.AnnotationRiskLow, // Low, as it allows just a set of options
Documentation: `Limits the rate of response transmission to a client. The rate is specified in bytes per second.
The zero value disables rate limiting. The limit is set per a request, and so if a client simultaneously opens two connections, the overall rate will be twice as much as the specified limit.
References: https://nginx.org/en/docs/http/ngx_http_core_module.html#limit_rate`,
},
limitRateAfterAnnotation: {
Validator: parser.ValidateInt,
Scope: parser.AnnotationScopeLocation,
Risk: parser.AnnotationRiskLow, // Low, as it allows just a set of options
Documentation: `Sets the initial amount after which the further transmission of a response to a client will be rate limited.`,
},
limitRateRPMAnnotation: {
Validator: parser.ValidateInt,
Scope: parser.AnnotationScopeLocation,
Risk: parser.AnnotationRiskLow, // Low, as it allows just a set of options
Documentation: `Requests per minute that will be allowed.`,
},
limitRateRPSAnnotation: {
Validator: parser.ValidateInt,
Scope: parser.AnnotationScopeLocation,
Risk: parser.AnnotationRiskLow, // Low, as it allows just a set of options
Documentation: `Requests per second that will be allowed.`,
},
limitRateConnectionsAnnotation: {
Validator: parser.ValidateInt,
Scope: parser.AnnotationScopeLocation,
Risk: parser.AnnotationRiskLow, // Low, as it allows just a set of options
Documentation: `Number of connections that will be allowed`,
},
limitRateBurstMultiplierAnnotation: {
Validator: parser.ValidateInt,
Scope: parser.AnnotationScopeLocation,
Risk: parser.AnnotationRiskLow, // Low, as it allows just a set of options
Documentation: `Burst multiplier for a limit-rate enabled location.`,
},
limitAllowlistAnnotation: {
Validator: parser.ValidateCIDRs,
Scope: parser.AnnotationScopeLocation,
Risk: parser.AnnotationRiskLow, // Low, as it allows just a set of options
Documentation: `List of CIDR/IP addresses that will not be rate-limited.`,
AnnotationAliases: []string{limitWhitelistAnnotation},
},
},
}
type ratelimit struct {
r resolver.Resolver
r resolver.Resolver
annotationConfig parser.Annotation
}
// NewParser creates a new ratelimit annotation parser
func NewParser(r resolver.Resolver) parser.IngressAnnotation {
return ratelimit{r}
return ratelimit{
r: r,
annotationConfig: rateLimitAnnotations,
}
}
// ParseAnnotations parses the annotations contained in the ingress
// rule used to rewrite the defined paths
func (a ratelimit) Parse(ing *networking.Ingress) (interface{}, error) {
defBackend := a.r.GetDefaultBackend()
lr, err := parser.GetIntAnnotation("limit-rate", ing)
lr, err := parser.GetIntAnnotation(limitRateAnnotation, ing, a.annotationConfig.Annotations)
if err != nil {
lr = defBackend.LimitRate
}
lra, err := parser.GetIntAnnotation("limit-rate-after", ing)
lra, err := parser.GetIntAnnotation(limitRateAfterAnnotation, ing, a.annotationConfig.Annotations)
if err != nil {
lra = defBackend.LimitRateAfter
}
rpm, _ := parser.GetIntAnnotation("limit-rpm", ing)
rps, _ := parser.GetIntAnnotation("limit-rps", ing)
conn, _ := parser.GetIntAnnotation("limit-connections", ing)
burstMultiplier, err := parser.GetIntAnnotation("limit-burst-multiplier", ing)
rpm, err := parser.GetIntAnnotation(limitRateRPMAnnotation, ing, a.annotationConfig.Annotations)
if err != nil && errors.IsValidationError(err) {
return nil, err
}
rps, err := parser.GetIntAnnotation(limitRateRPSAnnotation, ing, a.annotationConfig.Annotations)
if err != nil && errors.IsValidationError(err) {
return nil, err
}
conn, err := parser.GetIntAnnotation(limitRateConnectionsAnnotation, ing, a.annotationConfig.Annotations)
if err != nil && errors.IsValidationError(err) {
return nil, err
}
burstMultiplier, err := parser.GetIntAnnotation(limitRateBurstMultiplierAnnotation, ing, a.annotationConfig.Annotations)
if err != nil {
burstMultiplier = defBurst
}
val, _ := parser.GetStringAnnotation("limit-whitelist", ing)
cidrs, err := net.ParseCIDRs(val)
if err != nil {
val, err := parser.GetStringAnnotation(limitAllowlistAnnotation, ing, a.annotationConfig.Annotations)
if err != nil && errors.IsValidationError(err) {
return nil, err
}
cidrs, errCidr := net.ParseCIDRs(val)
if errCidr != nil {
return nil, errCidr
}
if rpm == 0 && rps == 0 && conn == 0 {
return &Config{
Connections: Zone{},
@ -203,7 +282,7 @@ func (a ratelimit) Parse(ing *networking.Ingress) (interface{}, error) {
LimitRateAfter: lra,
Name: zoneName,
ID: encode(zoneName),
Whitelist: cidrs,
Allowlist: cidrs,
}, nil
}
@ -211,3 +290,12 @@ func encode(s string) string {
str := base64.URLEncoding.EncodeToString([]byte(s))
return strings.Replace(str, "=", "", -1)
}
func (a ratelimit) GetDocumentation() parser.AnnotationFields {
return a.annotationConfig.Annotations
}
func (a ratelimit) Validate(anns map[string]string) error {
maxrisk := parser.StringRiskToRisk(a.r.GetSecurityConfiguration().AnnotationsRiskLevel)
return parser.CheckAnnotationRisk(anns, maxrisk, rateLimitAnnotations.Annotations)
}

View file

@ -25,6 +25,7 @@ import (
"k8s.io/ingress-nginx/internal/ingress/annotations/parser"
"k8s.io/ingress-nginx/internal/ingress/defaults"
"k8s.io/ingress-nginx/internal/ingress/errors"
"k8s.io/ingress-nginx/internal/ingress/resolver"
)
@ -85,8 +86,8 @@ func (m mockBackend) GetDefaultBackend() defaults.Backend {
func TestWithoutAnnotations(t *testing.T) {
ing := buildIngress()
_, err := NewParser(mockBackend{}).Parse(ing)
if err != nil {
t.Error("unexpected error with ingress without annotations")
if err != nil && !errors.IsMissingAnnotations(err) {
t.Errorf("unexpected error with ingress without annotations: %s", err)
}
}
@ -94,22 +95,22 @@ func TestRateLimiting(t *testing.T) {
ing := buildIngress()
data := map[string]string{}
data[parser.GetAnnotationWithPrefix("limit-connections")] = "0"
data[parser.GetAnnotationWithPrefix("limit-rps")] = "0"
data[parser.GetAnnotationWithPrefix("limit-rpm")] = "0"
data[parser.GetAnnotationWithPrefix(limitRateConnectionsAnnotation)] = "0"
data[parser.GetAnnotationWithPrefix(limitRateRPSAnnotation)] = "0"
data[parser.GetAnnotationWithPrefix(limitRateRPMAnnotation)] = "0"
ing.SetAnnotations(data)
_, err := NewParser(mockBackend{}).Parse(ing)
if err != nil {
t.Errorf("unexpected error with invalid limits (0)")
t.Errorf("unexpected error with invalid limits (0): %s", err)
}
data = map[string]string{}
data[parser.GetAnnotationWithPrefix("limit-connections")] = "5"
data[parser.GetAnnotationWithPrefix("limit-rps")] = "100"
data[parser.GetAnnotationWithPrefix("limit-rpm")] = "10"
data[parser.GetAnnotationWithPrefix("limit-rate-after")] = "100"
data[parser.GetAnnotationWithPrefix("limit-rate")] = "10"
data[parser.GetAnnotationWithPrefix(limitRateConnectionsAnnotation)] = "5"
data[parser.GetAnnotationWithPrefix(limitRateRPSAnnotation)] = "100"
data[parser.GetAnnotationWithPrefix(limitRateRPMAnnotation)] = "10"
data[parser.GetAnnotationWithPrefix(limitRateAfterAnnotation)] = "100"
data[parser.GetAnnotationWithPrefix(limitRateAnnotation)] = "10"
ing.SetAnnotations(data)
@ -147,12 +148,12 @@ func TestRateLimiting(t *testing.T) {
}
data = map[string]string{}
data[parser.GetAnnotationWithPrefix("limit-connections")] = "5"
data[parser.GetAnnotationWithPrefix("limit-rps")] = "100"
data[parser.GetAnnotationWithPrefix("limit-rpm")] = "10"
data[parser.GetAnnotationWithPrefix("limit-rate-after")] = "100"
data[parser.GetAnnotationWithPrefix("limit-rate")] = "10"
data[parser.GetAnnotationWithPrefix("limit-burst-multiplier")] = "3"
data[parser.GetAnnotationWithPrefix(limitRateConnectionsAnnotation)] = "5"
data[parser.GetAnnotationWithPrefix(limitRateRPSAnnotation)] = "100"
data[parser.GetAnnotationWithPrefix(limitRateRPMAnnotation)] = "10"
data[parser.GetAnnotationWithPrefix(limitRateAfterAnnotation)] = "100"
data[parser.GetAnnotationWithPrefix(limitRateAnnotation)] = "10"
data[parser.GetAnnotationWithPrefix(limitRateBurstMultiplierAnnotation)] = "3"
ing.SetAnnotations(data)
@ -189,3 +190,61 @@ func TestRateLimiting(t *testing.T) {
t.Errorf("expected 10 in limit by limitrate but %v was returned", rateLimit.LimitRate)
}
}
func TestAnnotationCIDR(t *testing.T) {
ing := buildIngress()
data := map[string]string{}
data[parser.GetAnnotationWithPrefix(limitRateConnectionsAnnotation)] = "5"
data[parser.GetAnnotationWithPrefix(limitAllowlistAnnotation)] = "192.168.0.5, 192.168.50.32/24"
ing.SetAnnotations(data)
i, err := NewParser(mockBackend{}).Parse(ing)
if err != nil {
t.Errorf("unexpected error: %v", err)
}
rateLimit, ok := i.(*Config)
if !ok {
t.Errorf("expected a RateLimit type")
}
if len(rateLimit.Allowlist) != 2 {
t.Errorf("expected 2 cidrs in limit by ip but %v was returned", len(rateLimit.Allowlist))
}
data = map[string]string{}
data[parser.GetAnnotationWithPrefix(limitRateConnectionsAnnotation)] = "5"
data[parser.GetAnnotationWithPrefix(limitWhitelistAnnotation)] = "192.168.0.5, 192.168.50.32/24, 10.10.10.1"
ing.SetAnnotations(data)
i, err = NewParser(mockBackend{}).Parse(ing)
if err != nil {
t.Errorf("unexpected error: %v", err)
}
rateLimit, ok = i.(*Config)
if !ok {
t.Errorf("expected a RateLimit type")
}
if len(rateLimit.Allowlist) != 3 {
t.Errorf("expected 3 cidrs in limit by ip but %v was returned", len(rateLimit.Allowlist))
}
// Parent annotation surpasses any alias
data = map[string]string{}
data[parser.GetAnnotationWithPrefix(limitRateConnectionsAnnotation)] = "5"
data[parser.GetAnnotationWithPrefix(limitWhitelistAnnotation)] = "192.168.0.5, 192.168.50.32/24, 10.10.10.1"
data[parser.GetAnnotationWithPrefix(limitAllowlistAnnotation)] = "192.168.0.9"
ing.SetAnnotations(data)
i, err = NewParser(mockBackend{}).Parse(ing)
if err != nil {
t.Errorf("unexpected error: %v", err)
}
rateLimit, ok = i.(*Config)
if !ok {
t.Errorf("expected a RateLimit type")
}
if len(rateLimit.Allowlist) != 1 {
t.Errorf("expected 1 cidrs in limit by ip but %v was returned", len(rateLimit.Allowlist))
}
}

View file

@ -37,13 +37,56 @@ type Config struct {
FromToWWW bool `json:"fromToWWW"`
}
const (
fromToWWWRedirAnnotation = "from-to-www-redirect"
temporalRedirectAnnotation = "temporal-redirect"
permanentRedirectAnnotation = "permanent-redirect"
permanentRedirectAnnotationCode = "permanent-redirect-code"
)
var redirectAnnotations = parser.Annotation{
Group: "redirect",
Annotations: parser.AnnotationFields{
fromToWWWRedirAnnotation: {
Validator: parser.ValidateBool,
Scope: parser.AnnotationScopeLocation,
Risk: parser.AnnotationRiskLow, // Low, as it allows just a set of options
Documentation: `In some scenarios is required to redirect from www.domain.com to domain.com or vice versa. To enable this feature use this annotation.`,
},
temporalRedirectAnnotation: {
Validator: parser.ValidateRegex(*parser.URLIsValidRegex, false),
Scope: parser.AnnotationScopeLocation,
Risk: parser.AnnotationRiskMedium, // Medium, as it allows arbitrary URLs that needs to be validated
Documentation: `This annotation allows you to return a temporal redirect (Return Code 302) instead of sending data to the upstream.
For example setting this annotation to https://www.google.com would redirect everything to Google with a Return Code of 302 (Moved Temporarily).`,
},
permanentRedirectAnnotation: {
Validator: parser.ValidateRegex(*parser.URLIsValidRegex, false),
Scope: parser.AnnotationScopeLocation,
Risk: parser.AnnotationRiskMedium, // Medium, as it allows arbitrary URLs that needs to be validated
Documentation: `This annotation allows to return a permanent redirect (Return Code 301) instead of sending data to the upstream.
For example setting this annotation https://www.google.com would redirect everything to Google with a code 301`,
},
permanentRedirectAnnotationCode: {
Validator: parser.ValidateInt,
Scope: parser.AnnotationScopeLocation,
Risk: parser.AnnotationRiskLow, // Low, as it allows just a set of options
Documentation: `This annotation allows you to modify the status code used for permanent redirects.`,
},
},
}
type redirect struct {
r resolver.Resolver
r resolver.Resolver
annotationConfig parser.Annotation
}
// NewParser creates a new redirect annotation parser
func NewParser(r resolver.Resolver) parser.IngressAnnotation {
return redirect{r}
return redirect{
r: r,
annotationConfig: redirectAnnotations,
}
}
// Parse parses the annotations contained in the ingress
@ -51,9 +94,12 @@ func NewParser(r resolver.Resolver) parser.IngressAnnotation {
// If the Ingress contains both annotations the execution order is
// temporal and then permanent
func (r redirect) Parse(ing *networking.Ingress) (interface{}, error) {
r3w, _ := parser.GetBoolAnnotation("from-to-www-redirect", ing)
r3w, err := parser.GetBoolAnnotation(fromToWWWRedirAnnotation, ing, r.annotationConfig.Annotations)
if err != nil && !errors.IsMissingAnnotations(err) {
return nil, err
}
tr, err := parser.GetStringAnnotation("temporal-redirect", ing)
tr, err := parser.GetStringAnnotation(temporalRedirectAnnotation, ing, r.annotationConfig.Annotations)
if err != nil && !errors.IsMissingAnnotations(err) {
return nil, err
}
@ -70,12 +116,12 @@ func (r redirect) Parse(ing *networking.Ingress) (interface{}, error) {
}, nil
}
pr, err := parser.GetStringAnnotation("permanent-redirect", ing)
pr, err := parser.GetStringAnnotation(permanentRedirectAnnotation, ing, r.annotationConfig.Annotations)
if err != nil && !errors.IsMissingAnnotations(err) {
return nil, err
}
prc, err := parser.GetIntAnnotation("permanent-redirect-code", ing)
prc, err := parser.GetIntAnnotation(permanentRedirectAnnotationCode, ing, r.annotationConfig.Annotations)
if err != nil && !errors.IsMissingAnnotations(err) {
return nil, err
}
@ -127,3 +173,12 @@ func isValidURL(s string) error {
return nil
}
func (a redirect) GetDocumentation() parser.AnnotationFields {
return a.annotationConfig.Annotations
}
func (a redirect) Validate(anns map[string]string) error {
maxrisk := parser.StringRiskToRisk(a.r.GetSecurityConfiguration().AnnotationsRiskLevel)
return parser.CheckAnnotationRisk(anns, maxrisk, redirectAnnotations.Annotations)
}

View file

@ -43,7 +43,7 @@ func TestPermanentRedirectWithDefaultCode(t *testing.T) {
ing := new(networking.Ingress)
data := make(map[string]string, 1)
data[parser.GetAnnotationWithPrefix("permanent-redirect")] = defRedirectURL
data[parser.GetAnnotationWithPrefix(permanentRedirectAnnotation)] = defRedirectURL
ing.SetAnnotations(data)
i, err := rp.Parse(ing)
@ -81,8 +81,8 @@ func TestPermanentRedirectWithCustomCode(t *testing.T) {
ing := new(networking.Ingress)
data := make(map[string]string, 2)
data[parser.GetAnnotationWithPrefix("permanent-redirect")] = defRedirectURL
data[parser.GetAnnotationWithPrefix("permanent-redirect-code")] = strconv.Itoa(tc.input)
data[parser.GetAnnotationWithPrefix(permanentRedirectAnnotation)] = defRedirectURL
data[parser.GetAnnotationWithPrefix(permanentRedirectAnnotationCode)] = strconv.Itoa(tc.input)
ing.SetAnnotations(data)
i, err := rp.Parse(ing)
@ -112,8 +112,8 @@ func TestTemporalRedirect(t *testing.T) {
ing := new(networking.Ingress)
data := make(map[string]string, 1)
data[parser.GetAnnotationWithPrefix("from-to-www-redirect")] = "true"
data[parser.GetAnnotationWithPrefix("temporal-redirect")] = defRedirectURL
data[parser.GetAnnotationWithPrefix(fromToWWWRedirAnnotation)] = "true"
data[parser.GetAnnotationWithPrefix(temporalRedirectAnnotation)] = defRedirectURL
ing.SetAnnotations(data)
i, err := rp.Parse(ing)

View file

@ -27,6 +27,59 @@ import (
"k8s.io/ingress-nginx/internal/ingress/resolver"
)
const (
rewriteTargetAnnotation = "rewrite-target"
sslRedirectAnnotation = "ssl-redirect"
preserveTrailingSlashAnnotation = "preserve-trailing-slash"
forceSSLRedirectAnnotation = "force-ssl-redirect"
useRegexAnnotation = "use-regex"
appRootAnnotation = "app-root"
)
var rewriteAnnotations = parser.Annotation{
Group: "rewrite",
Annotations: parser.AnnotationFields{
rewriteTargetAnnotation: {
Validator: parser.ValidateRegex(*parser.RegexPathWithCapture, false),
Scope: parser.AnnotationScopeIngress,
Risk: parser.AnnotationRiskMedium,
Documentation: `This annotation allows to specify the target URI where the traffic must be redirected. It can contain regular characters and captured
groups specified as '$1', '$2', etc.`,
},
sslRedirectAnnotation: {
Validator: parser.ValidateBool,
Scope: parser.AnnotationScopeLocation,
Risk: parser.AnnotationRiskLow,
Documentation: `This annotation defines if the location section is only accessible via SSL`,
},
preserveTrailingSlashAnnotation: {
Validator: parser.ValidateBool,
Scope: parser.AnnotationScopeLocation,
Risk: parser.AnnotationRiskMedium,
Documentation: `This annotation defines if the trailing slash should be preserved in the URI with 'ssl-redirect'`,
},
forceSSLRedirectAnnotation: {
Validator: parser.ValidateBool,
Scope: parser.AnnotationScopeLocation,
Risk: parser.AnnotationRiskMedium,
Documentation: `This annotation forces the redirection to HTTPS even if the Ingress is not TLS Enabled`,
},
useRegexAnnotation: {
Validator: parser.ValidateBool,
Scope: parser.AnnotationScopeLocation,
Risk: parser.AnnotationRiskLow,
Documentation: `This annotation defines if the paths defined on an Ingress use regular expressions. To use regex on path
the pathType should also be defined as 'ImplementationSpecific'.`,
},
appRootAnnotation: {
Validator: parser.ValidateRegex(*parser.RegexPathWithCapture, false),
Scope: parser.AnnotationScopeLocation,
Risk: parser.AnnotationRiskMedium,
Documentation: `This annotation defines the Application Root that the Controller must redirect if it's in / context`,
},
},
}
// Config describes the per location redirect config
type Config struct {
// Target URI where the traffic must be redirected
@ -71,12 +124,16 @@ func (r1 *Config) Equal(r2 *Config) bool {
}
type rewrite struct {
r resolver.Resolver
r resolver.Resolver
annotationConfig parser.Annotation
}
// NewParser creates a new rewrite annotation parser
func NewParser(r resolver.Resolver) parser.IngressAnnotation {
return rewrite{r}
return rewrite{
r: r,
annotationConfig: rewriteAnnotations,
}
}
// ParseAnnotations parses the annotations contained in the ingress
@ -85,24 +142,45 @@ func (a rewrite) Parse(ing *networking.Ingress) (interface{}, error) {
var err error
config := &Config{}
config.Target, _ = parser.GetStringAnnotation("rewrite-target", ing)
config.SSLRedirect, err = parser.GetBoolAnnotation("ssl-redirect", ing)
config.Target, err = parser.GetStringAnnotation(rewriteTargetAnnotation, ing, a.annotationConfig.Annotations)
if err != nil {
if errors.IsValidationError(err) {
klog.Warningf("%sis invalid, defaulting to empty", rewriteTargetAnnotation)
}
config.Target = ""
}
config.SSLRedirect, err = parser.GetBoolAnnotation(sslRedirectAnnotation, ing, a.annotationConfig.Annotations)
if err != nil {
if errors.IsValidationError(err) {
klog.Warningf("%sis invalid, defaulting to '%s'", sslRedirectAnnotation, a.r.GetDefaultBackend().SSLRedirect)
}
config.SSLRedirect = a.r.GetDefaultBackend().SSLRedirect
}
config.PreserveTrailingSlash, err = parser.GetBoolAnnotation("preserve-trailing-slash", ing)
config.PreserveTrailingSlash, err = parser.GetBoolAnnotation(preserveTrailingSlashAnnotation, ing, a.annotationConfig.Annotations)
if err != nil {
if errors.IsValidationError(err) {
klog.Warningf("%sis invalid, defaulting to '%s'", preserveTrailingSlashAnnotation, a.r.GetDefaultBackend().PreserveTrailingSlash)
}
config.PreserveTrailingSlash = a.r.GetDefaultBackend().PreserveTrailingSlash
}
config.ForceSSLRedirect, err = parser.GetBoolAnnotation("force-ssl-redirect", ing)
config.ForceSSLRedirect, err = parser.GetBoolAnnotation(forceSSLRedirectAnnotation, ing, a.annotationConfig.Annotations)
if err != nil {
if errors.IsValidationError(err) {
klog.Warningf("%sis invalid, defaulting to '%s'", forceSSLRedirectAnnotation, a.r.GetDefaultBackend().ForceSSLRedirect)
}
config.ForceSSLRedirect = a.r.GetDefaultBackend().ForceSSLRedirect
}
config.UseRegex, _ = parser.GetBoolAnnotation("use-regex", ing)
config.UseRegex, err = parser.GetBoolAnnotation(useRegexAnnotation, ing, a.annotationConfig.Annotations)
if err != nil {
if errors.IsValidationError(err) {
klog.Warningf("%sis invalid, defaulting to 'false'", useRegexAnnotation)
}
config.UseRegex = false
}
config.AppRoot, err = parser.GetStringAnnotation("app-root", ing)
config.AppRoot, err = parser.GetStringAnnotation(appRootAnnotation, ing, a.annotationConfig.Annotations)
if err != nil {
if !errors.IsMissingAnnotations(err) && !errors.IsInvalidContent(err) {
klog.Warningf("Annotation app-root contains an invalid value: %v", err)
@ -126,3 +204,12 @@ func (a rewrite) Parse(ing *networking.Ingress) (interface{}, error) {
return config, nil
}
func (a rewrite) GetDocumentation() parser.AnnotationFields {
return a.annotationConfig.Annotations
}
func (a rewrite) Validate(anns map[string]string) error {
maxrisk := parser.StringRiskToRisk(a.r.GetSecurityConfiguration().AnnotationsRiskLevel)
return parser.CheckAnnotationRisk(anns, maxrisk, rewriteAnnotations.Annotations)
}

View file

@ -129,6 +129,30 @@ func TestSSLRedirect(t *testing.T) {
t.Errorf("Expected true but returned false")
}
data[parser.GetAnnotationWithPrefix("rewrite-target")] = "/xpto/$1/abc/$2"
ing.SetAnnotations(data)
i, _ = NewParser(mockBackend{redirect: true}).Parse(ing)
redirect, ok = i.(*Config)
if !ok {
t.Errorf("expected a Redirect type")
}
if redirect.Target != "/xpto/$1/abc/$2" {
t.Errorf("Expected /xpto/$1/abc/$2 but returned %s", redirect.Target)
}
data[parser.GetAnnotationWithPrefix("rewrite-target")] = "/xpto/xas{445}"
ing.SetAnnotations(data)
i, _ = NewParser(mockBackend{redirect: true}).Parse(ing)
redirect, ok = i.(*Config)
if !ok {
t.Errorf("expected a Redirect type")
}
if redirect.Target != "" {
t.Errorf("Expected empty rewrite target but returned %s", redirect.Target)
}
data[parser.GetAnnotationWithPrefix("ssl-redirect")] = "false"
ing.SetAnnotations(data)

View file

@ -23,18 +23,40 @@ import (
"k8s.io/ingress-nginx/internal/ingress/resolver"
)
const (
satisfyAnnotation = "satisfy"
)
var satisfyAnnotations = parser.Annotation{
Group: "authentication",
Annotations: parser.AnnotationFields{
satisfyAnnotation: {
Validator: parser.ValidateOptions([]string{"any", "all"}, true, true),
Scope: parser.AnnotationScopeLocation,
Risk: parser.AnnotationRiskLow,
Documentation: `By default, a request would need to satisfy all authentication requirements in order to be allowed.
By using this annotation, requests that satisfy either any or all authentication requirements are allowed, based on the configuration value.
Valid options are "all" and "any"`,
},
},
}
type satisfy struct {
r resolver.Resolver
r resolver.Resolver
annotationConfig parser.Annotation
}
// NewParser creates a new SATISFY annotation parser
func NewParser(r resolver.Resolver) parser.IngressAnnotation {
return satisfy{r}
return satisfy{
r: r,
annotationConfig: satisfyAnnotations,
}
}
// Parse parses annotation contained in the ingress
func (s satisfy) Parse(ing *networking.Ingress) (interface{}, error) {
satisfy, err := parser.GetStringAnnotation("satisfy", ing)
satisfy, err := parser.GetStringAnnotation(satisfyAnnotation, ing, s.annotationConfig.Annotations)
if err != nil || (satisfy != "any" && satisfy != "all") {
satisfy = ""
@ -42,3 +64,12 @@ func (s satisfy) Parse(ing *networking.Ingress) (interface{}, error) {
return satisfy, nil
}
func (s satisfy) GetDocumentation() parser.AnnotationFields {
return s.annotationConfig.Annotations
}
func (a satisfy) Validate(anns map[string]string) error {
maxrisk := parser.StringRiskToRisk(a.r.GetSecurityConfiguration().AnnotationsRiskLevel)
return parser.CheckAnnotationRisk(anns, maxrisk, satisfyAnnotations.Annotations)
}

View file

@ -83,7 +83,7 @@ func TestSatisfyParser(t *testing.T) {
annotations := map[string]string{}
for input, expected := range data {
annotations[parser.GetAnnotationWithPrefix("satisfy")] = input
annotations[parser.GetAnnotationWithPrefix(satisfyAnnotation)] = input
ing.SetAnnotations(annotations)
satisfyt, err := NewParser(&resolver.Mock{}).Parse(ing)

View file

@ -23,18 +23,47 @@ import (
"k8s.io/ingress-nginx/internal/ingress/resolver"
)
const (
serverSnippetAnnotation = "server-snippet"
)
var serverSnippetAnnotations = parser.Annotation{
Group: "snippets",
Annotations: parser.AnnotationFields{
serverSnippetAnnotation: {
Validator: parser.ValidateNull,
Scope: parser.AnnotationScopeIngress,
Risk: parser.AnnotationRiskCritical, // Critical, this annotation is not validated at all and allows arbitrary configutations
Documentation: `This annotation allows setting a custom NGINX configuration on a server block. This annotation does not contain any validation and it's usage is not recommended!`,
},
},
}
type serverSnippet struct {
r resolver.Resolver
r resolver.Resolver
annotationConfig parser.Annotation
}
// NewParser creates a new server snippet annotation parser
func NewParser(r resolver.Resolver) parser.IngressAnnotation {
return serverSnippet{r}
return serverSnippet{
r: r,
annotationConfig: serverSnippetAnnotations,
}
}
// Parse parses the annotations contained in the ingress rule
// used to indicate if the location/s contains a fragment of
// configuration to be included inside the paths of the rules
func (a serverSnippet) Parse(ing *networking.Ingress) (interface{}, error) {
return parser.GetStringAnnotation("server-snippet", ing)
return parser.GetStringAnnotation(serverSnippetAnnotation, ing, a.annotationConfig.Annotations)
}
func (a serverSnippet) GetDocumentation() parser.AnnotationFields {
return a.annotationConfig.Annotations
}
func (a serverSnippet) Validate(anns map[string]string) error {
maxrisk := parser.StringRiskToRisk(a.r.GetSecurityConfiguration().AnnotationsRiskLevel)
return parser.CheckAnnotationRisk(anns, maxrisk, serverSnippetAnnotations.Annotations)
}

View file

@ -27,7 +27,7 @@ import (
)
func TestParse(t *testing.T) {
annotation := parser.GetAnnotationWithPrefix("server-snippet")
annotation := parser.GetAnnotationWithPrefix(serverSnippetAnnotation)
ap := NewParser(&resolver.Mock{})
if ap == nil {

View file

@ -24,19 +24,39 @@ import (
"k8s.io/ingress-nginx/internal/ingress/resolver"
)
const (
serviceUpstreamAnnotation = "service-upstream"
)
var serviceUpstreamAnnotations = parser.Annotation{
Group: "backend",
Annotations: parser.AnnotationFields{
serviceUpstreamAnnotation: {
Validator: parser.ValidateBool,
Scope: parser.AnnotationScopeIngress,
Risk: parser.AnnotationRiskLow, // Critical, this annotation is not validated at all and allows arbitrary configutations
Documentation: `This annotation makes NGINX use Service's Cluster IP and Port instead of Endpoints as the backend endpoints`,
},
},
}
type serviceUpstream struct {
r resolver.Resolver
r resolver.Resolver
annotationConfig parser.Annotation
}
// NewParser creates a new serviceUpstream annotation parser
func NewParser(r resolver.Resolver) parser.IngressAnnotation {
return serviceUpstream{r}
return serviceUpstream{
r: r,
annotationConfig: serviceUpstreamAnnotations,
}
}
func (s serviceUpstream) Parse(ing *networking.Ingress) (interface{}, error) {
defBackend := s.r.GetDefaultBackend()
val, err := parser.GetBoolAnnotation("service-upstream", ing)
val, err := parser.GetBoolAnnotation(serviceUpstreamAnnotation, ing, s.annotationConfig.Annotations)
// A missing annotation is not a problem, just use the default
if err == errors.ErrMissingAnnotations {
return defBackend.ServiceUpstream, nil
@ -44,3 +64,12 @@ func (s serviceUpstream) Parse(ing *networking.Ingress) (interface{}, error) {
return val, nil
}
func (s serviceUpstream) GetDocumentation() parser.AnnotationFields {
return s.annotationConfig.Annotations
}
func (a serviceUpstream) Validate(anns map[string]string) error {
maxrisk := parser.StringRiskToRisk(a.r.GetSecurityConfiguration().AnnotationsRiskLevel)
return parser.CheckAnnotationRisk(anns, maxrisk, serviceUpstreamAnnotations.Annotations)
}

View file

@ -74,7 +74,7 @@ func TestIngressAnnotationServiceUpstreamEnabled(t *testing.T) {
ing := buildIngress()
data := map[string]string{}
data[parser.GetAnnotationWithPrefix("service-upstream")] = "true"
data[parser.GetAnnotationWithPrefix(serviceUpstreamAnnotation)] = "true"
ing.SetAnnotations(data)
val, _ := NewParser(&resolver.Mock{}).Parse(ing)
@ -93,7 +93,7 @@ func TestIngressAnnotationServiceUpstreamSetFalse(t *testing.T) {
// Test with explicitly set to false
data := map[string]string{}
data[parser.GetAnnotationWithPrefix("service-upstream")] = "false"
data[parser.GetAnnotationWithPrefix(serviceUpstreamAnnotation)] = "false"
ing.SetAnnotations(data)
val, _ := NewParser(&resolver.Mock{}).Parse(ing)
@ -155,7 +155,7 @@ func TestParseAnnotationsOverridesDefaultConfig(t *testing.T) {
ing := buildIngress()
data := map[string]string{}
data[parser.GetAnnotationWithPrefix("service-upstream")] = "false"
data[parser.GetAnnotationWithPrefix(serviceUpstreamAnnotation)] = "false"
ing.SetAnnotations(data)
val, _ := NewParser(mockBackend{}).Parse(ing)

View file

@ -65,6 +65,90 @@ const (
annotationAffinityCookieChangeOnFailure = "session-cookie-change-on-failure"
)
var sessionAffinityAnnotations = parser.Annotation{
Group: "affinity",
Annotations: parser.AnnotationFields{
annotationAffinityType: {
Validator: parser.ValidateOptions([]string{"cookie"}, true, true),
Scope: parser.AnnotationScopeIngress,
Risk: parser.AnnotationRiskLow,
Documentation: `This annotation enables and sets the affinity type in all Upstreams of an Ingress. This way, a request will always be directed to the same upstream server. The only affinity type available for NGINX is cookie`,
},
annotationAffinityMode: {
Validator: parser.ValidateOptions([]string{"balanced", "persistent"}, true, true),
Scope: parser.AnnotationScopeIngress,
Risk: parser.AnnotationRiskMedium,
Documentation: `This annotation defines the stickiness of a session.
Setting this to balanced (default) will redistribute some sessions if a deployment gets scaled up, therefore rebalancing the load on the servers.
Setting this to persistent will not rebalance sessions to new servers, therefore providing maximum stickiness.`,
},
annotationAffinityCanaryBehavior: {
Validator: parser.ValidateOptions([]string{"sticky", "legacy"}, true, true),
Scope: parser.AnnotationScopeIngress,
Risk: parser.AnnotationRiskLow,
Documentation: `This annotation defines the behavior of canaries when session affinity is enabled.
Setting this to sticky (default) will ensure that users that were served by canaries, will continue to be served by canaries.
Setting this to legacy will restore original canary behavior, when session affinity was ignored.`,
},
annotationAffinityCookieName: {
Validator: parser.ValidateRegex(*parser.BasicCharsRegex, true),
Scope: parser.AnnotationScopeIngress,
Risk: parser.AnnotationRiskMedium,
Documentation: `This annotation allows to specify the name of the cookie that will be used to route the requests`,
},
annotationAffinityCookieSecure: {
Validator: parser.ValidateBool,
Scope: parser.AnnotationScopeIngress,
Risk: parser.AnnotationRiskLow,
Documentation: `This annotation set the cookie as secure regardless the protocol of the incoming request`,
},
annotationAffinityCookieExpires: {
Validator: parser.ValidateRegex(*affinityCookieExpiresRegex, true),
Scope: parser.AnnotationScopeIngress,
Risk: parser.AnnotationRiskMedium,
Documentation: `This annotation is a legacy version of "session-cookie-max-age" for compatibility with older browsers, generates an "Expires" cookie directive by adding the seconds to the current date`,
},
annotationAffinityCookieMaxAge: {
Validator: parser.ValidateRegex(*affinityCookieExpiresRegex, false),
Scope: parser.AnnotationScopeIngress,
Risk: parser.AnnotationRiskMedium,
Documentation: `This annotation sets the time until the cookie expires`,
},
annotationAffinityCookiePath: {
Validator: parser.ValidateRegex(*parser.URLIsValidRegex, true),
Scope: parser.AnnotationScopeIngress,
Risk: parser.AnnotationRiskMedium,
Documentation: `This annotation defines the Path that will be set on the cookie (required if your Ingress paths use regular expressions)`,
},
annotationAffinityCookieDomain: {
Validator: parser.ValidateRegex(*parser.BasicCharsRegex, true),
Scope: parser.AnnotationScopeIngress,
Risk: parser.AnnotationRiskMedium,
Documentation: `This annotation defines the Domain attribute of the sticky cookie.`,
},
annotationAffinityCookieSameSite: {
Validator: parser.ValidateOptions([]string{"None", "Lax", "Strict"}, false, true),
Scope: parser.AnnotationScopeIngress,
Risk: parser.AnnotationRiskLow,
Documentation: `This annotation is used to apply a SameSite attribute to the sticky cookie.
Browser accepted values are None, Lax, and Strict`,
},
annotationAffinityCookieConditionalSameSiteNone: {
Validator: parser.ValidateBool,
Scope: parser.AnnotationScopeIngress,
Risk: parser.AnnotationRiskLow,
Documentation: `This annotation is used to omit SameSite=None from browsers with SameSite attribute incompatibilities`,
},
annotationAffinityCookieChangeOnFailure: {
Validator: parser.ValidateBool,
Scope: parser.AnnotationScopeIngress,
Risk: parser.AnnotationRiskLow,
Documentation: `This annotation, when set to false will send request to upstream pointed by sticky cookie even if previous attempt failed.
When set to true and previous attempt failed, sticky cookie will be changed to point to another upstream.`,
},
},
}
var (
affinityCookieExpiresRegex = regexp.MustCompile(`(^0|-?[1-9]\d*$)`)
)
@ -109,50 +193,50 @@ func (a affinity) cookieAffinityParse(ing *networking.Ingress) *Cookie {
cookie := &Cookie{}
cookie.Name, err = parser.GetStringAnnotation(annotationAffinityCookieName, ing)
cookie.Name, err = parser.GetStringAnnotation(annotationAffinityCookieName, ing, a.annotationConfig.Annotations)
if err != nil {
klog.V(3).InfoS("Invalid or no annotation value found. Ignoring", "ingress", klog.KObj(ing), "annotation", annotationAffinityCookieName, "default", defaultAffinityCookieName)
cookie.Name = defaultAffinityCookieName
}
cookie.Expires, err = parser.GetStringAnnotation(annotationAffinityCookieExpires, ing)
cookie.Expires, err = parser.GetStringAnnotation(annotationAffinityCookieExpires, ing, a.annotationConfig.Annotations)
if err != nil || !affinityCookieExpiresRegex.MatchString(cookie.Expires) {
klog.V(3).InfoS("Invalid or no annotation value found. Ignoring", "ingress", klog.KObj(ing), "annotation", annotationAffinityCookieExpires)
cookie.Expires = ""
}
cookie.MaxAge, err = parser.GetStringAnnotation(annotationAffinityCookieMaxAge, ing)
cookie.MaxAge, err = parser.GetStringAnnotation(annotationAffinityCookieMaxAge, ing, a.annotationConfig.Annotations)
if err != nil || !affinityCookieExpiresRegex.MatchString(cookie.MaxAge) {
klog.V(3).InfoS("Invalid or no annotation value found. Ignoring", "ingress", klog.KObj(ing), "annotation", annotationAffinityCookieMaxAge)
cookie.MaxAge = ""
}
cookie.Path, err = parser.GetStringAnnotation(annotationAffinityCookiePath, ing)
cookie.Path, err = parser.GetStringAnnotation(annotationAffinityCookiePath, ing, a.annotationConfig.Annotations)
if err != nil {
klog.V(3).InfoS("Invalid or no annotation value found. Ignoring", "ingress", klog.KObj(ing), "annotation", annotationAffinityCookiePath)
}
cookie.Domain, err = parser.GetStringAnnotation(annotationAffinityCookieDomain, ing)
cookie.Domain, err = parser.GetStringAnnotation(annotationAffinityCookieDomain, ing, a.annotationConfig.Annotations)
if err != nil {
klog.V(3).InfoS("Invalid or no annotation value found. Ignoring", "ingress", klog.KObj(ing), "annotation", annotationAffinityCookieDomain)
}
cookie.SameSite, err = parser.GetStringAnnotation(annotationAffinityCookieSameSite, ing)
cookie.SameSite, err = parser.GetStringAnnotation(annotationAffinityCookieSameSite, ing, a.annotationConfig.Annotations)
if err != nil {
klog.V(3).InfoS("Invalid or no annotation value found. Ignoring", "ingress", klog.KObj(ing), "annotation", annotationAffinityCookieSameSite)
}
cookie.Secure, err = parser.GetBoolAnnotation(annotationAffinityCookieSecure, ing)
cookie.Secure, err = parser.GetBoolAnnotation(annotationAffinityCookieSecure, ing, a.annotationConfig.Annotations)
if err != nil {
klog.V(3).InfoS("Invalid or no annotation value found. Ignoring", "ingress", klog.KObj(ing), "annotation", annotationAffinityCookieSecure)
}
cookie.ConditionalSameSiteNone, err = parser.GetBoolAnnotation(annotationAffinityCookieConditionalSameSiteNone, ing)
cookie.ConditionalSameSiteNone, err = parser.GetBoolAnnotation(annotationAffinityCookieConditionalSameSiteNone, ing, a.annotationConfig.Annotations)
if err != nil {
klog.V(3).InfoS("Invalid or no annotation value found. Ignoring", "ingress", klog.KObj(ing), "annotation", annotationAffinityCookieConditionalSameSiteNone)
}
cookie.ChangeOnFailure, err = parser.GetBoolAnnotation(annotationAffinityCookieChangeOnFailure, ing)
cookie.ChangeOnFailure, err = parser.GetBoolAnnotation(annotationAffinityCookieChangeOnFailure, ing, a.annotationConfig.Annotations)
if err != nil {
klog.V(3).InfoS("Invalid or no annotation value found. Ignoring", "ingress", klog.KObj(ing), "annotation", annotationAffinityCookieChangeOnFailure)
}
@ -162,11 +246,15 @@ func (a affinity) cookieAffinityParse(ing *networking.Ingress) *Cookie {
// NewParser creates a new Affinity annotation parser
func NewParser(r resolver.Resolver) parser.IngressAnnotation {
return affinity{r}
return affinity{
r: r,
annotationConfig: sessionAffinityAnnotations,
}
}
type affinity struct {
r resolver.Resolver
r resolver.Resolver
annotationConfig parser.Annotation
}
// ParseAnnotations parses the annotations contained in the ingress
@ -174,18 +262,18 @@ type affinity struct {
func (a affinity) Parse(ing *networking.Ingress) (interface{}, error) {
cookie := &Cookie{}
// Check the type of affinity that will be used
at, err := parser.GetStringAnnotation(annotationAffinityType, ing)
at, err := parser.GetStringAnnotation(annotationAffinityType, ing, a.annotationConfig.Annotations)
if err != nil {
at = ""
}
// Check the affinity mode that will be used
am, err := parser.GetStringAnnotation(annotationAffinityMode, ing)
am, err := parser.GetStringAnnotation(annotationAffinityMode, ing, a.annotationConfig.Annotations)
if err != nil {
am = ""
}
cb, err := parser.GetStringAnnotation(annotationAffinityCanaryBehavior, ing)
cb, err := parser.GetStringAnnotation(annotationAffinityCanaryBehavior, ing, a.annotationConfig.Annotations)
if err != nil {
cb = ""
}
@ -205,3 +293,12 @@ func (a affinity) Parse(ing *networking.Ingress) (interface{}, error) {
Cookie: *cookie,
}, nil
}
func (a affinity) GetDocumentation() parser.AnnotationFields {
return a.annotationConfig.Annotations
}
func (a affinity) Validate(anns map[string]string) error {
maxrisk := parser.StringRiskToRisk(a.r.GetSecurityConfiguration().AnnotationsRiskLevel)
return parser.CheckAnnotationRisk(anns, maxrisk, sessionAffinityAnnotations.Annotations)
}

View file

@ -23,18 +23,47 @@ import (
"k8s.io/ingress-nginx/internal/ingress/resolver"
)
const (
configurationSnippetAnnotation = "configuration-snippet"
)
var configurationSnippetAnnotations = parser.Annotation{
Group: "snippets",
Annotations: parser.AnnotationFields{
configurationSnippetAnnotation: {
Validator: parser.ValidateNull,
Scope: parser.AnnotationScopeLocation,
Risk: parser.AnnotationRiskCritical, // Critical, this annotation is not validated at all and allows arbitrary configutations
Documentation: `This annotation allows setting a custom NGINX configuration on a location block. This annotation does not contain any validation and it's usage is not recommended!`,
},
},
}
type snippet struct {
r resolver.Resolver
r resolver.Resolver
annotationConfig parser.Annotation
}
// NewParser creates a new CORS annotation parser
func NewParser(r resolver.Resolver) parser.IngressAnnotation {
return snippet{r}
return snippet{
r: r,
annotationConfig: configurationSnippetAnnotations,
}
}
// Parse parses the annotations contained in the ingress rule
// used to indicate if the location/s contains a fragment of
// configuration to be included inside the paths of the rules
func (a snippet) Parse(ing *networking.Ingress) (interface{}, error) {
return parser.GetStringAnnotation("configuration-snippet", ing)
return parser.GetStringAnnotation(configurationSnippetAnnotation, ing, a.annotationConfig.Annotations)
}
func (a snippet) GetDocumentation() parser.AnnotationFields {
return a.annotationConfig.Annotations
}
func (a snippet) Validate(anns map[string]string) error {
maxrisk := parser.StringRiskToRisk(a.r.GetSecurityConfiguration().AnnotationsRiskLevel)
return parser.CheckAnnotationRisk(anns, maxrisk, configurationSnippetAnnotations.Annotations)
}

View file

@ -27,7 +27,7 @@ import (
)
func TestParse(t *testing.T) {
annotation := parser.GetAnnotationWithPrefix("configuration-snippet")
annotation := parser.GetAnnotationWithPrefix(configurationSnippetAnnotation)
ap := NewParser(&resolver.Mock{})
if ap == nil {

View file

@ -17,14 +17,47 @@ limitations under the License.
package sslcipher
import (
"regexp"
networking "k8s.io/api/networking/v1"
"k8s.io/ingress-nginx/internal/ingress/annotations/parser"
"k8s.io/ingress-nginx/internal/ingress/errors"
"k8s.io/ingress-nginx/internal/ingress/resolver"
)
const (
sslPreferServerCipherAnnotation = "ssl-prefer-server-ciphers"
sslCipherAnnotation = "ssl-ciphers"
)
var (
// Should cover something like "ALL:!aNULL:!EXPORT56:RC4+RSA:+HIGH:+MEDIUM:+LOW:+SSLv2:+EXP"
regexValidSSLCipher = regexp.MustCompile(`^[A-Za-z0-9!:+\-]*$`)
)
var sslCipherAnnotations = parser.Annotation{
Group: "backend",
Annotations: parser.AnnotationFields{
sslPreferServerCipherAnnotation: {
Validator: parser.ValidateBool,
Scope: parser.AnnotationScopeIngress,
Risk: parser.AnnotationRiskLow,
Documentation: `The following annotation will set the ssl_prefer_server_ciphers directive at the server level.
This configuration specifies that server ciphers should be preferred over client ciphers when using the SSLv3 and TLS protocols.`,
},
sslCipherAnnotation: {
Validator: parser.ValidateRegex(*regexValidSSLCipher, true),
Scope: parser.AnnotationScopeIngress,
Risk: parser.AnnotationRiskLow,
Documentation: `Using this annotation will set the ssl_ciphers directive at the server level. This configuration is active for all the paths in the host.`,
},
},
}
type sslCipher struct {
r resolver.Resolver
r resolver.Resolver
annotationConfig parser.Annotation
}
// Config contains the ssl-ciphers & ssl-prefer-server-ciphers configuration
@ -35,7 +68,10 @@ type Config struct {
// NewParser creates a new sslCipher annotation parser
func NewParser(r resolver.Resolver) parser.IngressAnnotation {
return sslCipher{r}
return sslCipher{
r: r,
annotationConfig: sslCipherAnnotations,
}
}
// Parse parses the annotations contained in the ingress rule
@ -45,7 +81,7 @@ func (sc sslCipher) Parse(ing *networking.Ingress) (interface{}, error) {
var err error
var sslPreferServerCiphers bool
sslPreferServerCiphers, err = parser.GetBoolAnnotation("ssl-prefer-server-ciphers", ing)
sslPreferServerCiphers, err = parser.GetBoolAnnotation(sslPreferServerCipherAnnotation, ing, sc.annotationConfig.Annotations)
if err != nil {
config.SSLPreferServerCiphers = ""
} else {
@ -56,7 +92,19 @@ func (sc sslCipher) Parse(ing *networking.Ingress) (interface{}, error) {
}
}
config.SSLCiphers, _ = parser.GetStringAnnotation("ssl-ciphers", ing)
config.SSLCiphers, err = parser.GetStringAnnotation(sslCipherAnnotation, ing, sc.annotationConfig.Annotations)
if err != nil && !errors.IsInvalidContent(err) && !errors.IsMissingAnnotations(err) {
return config, err
}
return config, nil
}
func (sc sslCipher) GetDocumentation() parser.AnnotationFields {
return sc.annotationConfig.Annotations
}
func (a sslCipher) Validate(anns map[string]string) error {
maxrisk := parser.StringRiskToRisk(a.r.GetSecurityConfiguration().AnnotationsRiskLevel)
return parser.CheckAnnotationRisk(anns, maxrisk, sslCipherAnnotations.Annotations)
}

View file

@ -33,22 +33,24 @@ func TestParse(t *testing.T) {
t.Fatalf("expected a parser.IngressAnnotation but returned nil")
}
annotationSSLCiphers := parser.GetAnnotationWithPrefix("ssl-ciphers")
annotationSSLPreferServerCiphers := parser.GetAnnotationWithPrefix("ssl-prefer-server-ciphers")
annotationSSLCiphers := parser.GetAnnotationWithPrefix(sslCipherAnnotation)
annotationSSLPreferServerCiphers := parser.GetAnnotationWithPrefix(sslPreferServerCipherAnnotation)
testCases := []struct {
annotations map[string]string
expected Config
expectErr bool
}{
{map[string]string{annotationSSLCiphers: "ALL:!aNULL:!EXPORT56:RC4+RSA:+HIGH:+MEDIUM:+LOW:+SSLv2:+EXP"}, Config{"ALL:!aNULL:!EXPORT56:RC4+RSA:+HIGH:+MEDIUM:+LOW:+SSLv2:+EXP", ""}},
{map[string]string{annotationSSLCiphers: "ALL:!aNULL:!EXPORT56:RC4+RSA:+HIGH:+MEDIUM:+LOW:+SSLv2:+EXP"}, Config{"ALL:!aNULL:!EXPORT56:RC4+RSA:+HIGH:+MEDIUM:+LOW:+SSLv2:+EXP", ""}, false},
{map[string]string{annotationSSLCiphers: "ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-SHA384:ECDHE-RSA-AES256-SHA384:ECDHE-ECDSA-AES128-SHA256:ECDHE-RSA-AES128-SHA256"},
Config{"ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-SHA384:ECDHE-RSA-AES256-SHA384:ECDHE-ECDSA-AES128-SHA256:ECDHE-RSA-AES128-SHA256", ""}},
{map[string]string{annotationSSLCiphers: ""}, Config{"", ""}},
{map[string]string{annotationSSLPreferServerCiphers: "true"}, Config{"", "on"}},
{map[string]string{annotationSSLPreferServerCiphers: "false"}, Config{"", "off"}},
{map[string]string{annotationSSLCiphers: "ALL:!aNULL:!EXPORT56:RC4+RSA:+HIGH:+MEDIUM:+LOW:+SSLv2:+EXP", annotationSSLPreferServerCiphers: "true"}, Config{"ALL:!aNULL:!EXPORT56:RC4+RSA:+HIGH:+MEDIUM:+LOW:+SSLv2:+EXP", "on"}},
{map[string]string{}, Config{"", ""}},
{nil, Config{"", ""}},
Config{"ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-SHA384:ECDHE-RSA-AES256-SHA384:ECDHE-ECDSA-AES128-SHA256:ECDHE-RSA-AES128-SHA256", ""}, false},
{map[string]string{annotationSSLCiphers: ""}, Config{"", ""}, false},
{map[string]string{annotationSSLPreferServerCiphers: "true"}, Config{"", "on"}, false},
{map[string]string{annotationSSLPreferServerCiphers: "false"}, Config{"", "off"}, false},
{map[string]string{annotationSSLCiphers: "ALL:!aNULL:!EXPORT56:RC4+RSA:+HIGH:+MEDIUM:+LOW:+SSLv2:+EXP", annotationSSLPreferServerCiphers: "true"}, Config{"ALL:!aNULL:!EXPORT56:RC4+RSA:+HIGH:+MEDIUM:+LOW:+SSLv2:+EXP", "on"}, false},
{map[string]string{annotationSSLCiphers: "ALL:SOMETHING:;locationXPTO"}, Config{"", ""}, true},
{map[string]string{}, Config{"", ""}, false},
{nil, Config{"", ""}, false},
}
ing := &networking.Ingress{
@ -61,7 +63,10 @@ func TestParse(t *testing.T) {
for _, testCase := range testCases {
ing.SetAnnotations(testCase.annotations)
result, _ := ap.Parse(ing)
result, err := ap.Parse(ing)
if (err != nil) != testCase.expectErr {
t.Fatalf("expected error: %t got error: %t err value: %s. %+v", testCase.expectErr, err != nil, err, testCase.annotations)
}
if !reflect.DeepEqual(result, &testCase.expected) {
t.Errorf("expected %v but returned %v, annotations: %s", testCase.expected, result, testCase.annotations)
}

View file

@ -24,13 +24,32 @@ import (
"k8s.io/ingress-nginx/internal/ingress/resolver"
)
const (
sslPassthroughAnnotation = "ssl-passthrough"
)
var sslPassthroughAnnotations = parser.Annotation{
Group: "", // TBD
Annotations: parser.AnnotationFields{
sslPassthroughAnnotation: {
Validator: parser.ValidateBool,
Scope: parser.AnnotationScopeIngress,
Risk: parser.AnnotationRiskLow, // Low, as it allows regexes but on a very limited set
Documentation: `This annotation instructs the controller to send TLS connections directly to the backend instead of letting NGINX decrypt the communication.`,
},
},
}
type sslpt struct {
r resolver.Resolver
r resolver.Resolver
annotationConfig parser.Annotation
}
// NewParser creates a new SSL passthrough annotation parser
func NewParser(r resolver.Resolver) parser.IngressAnnotation {
return sslpt{r}
return sslpt{r: r,
annotationConfig: sslPassthroughAnnotations,
}
}
// ParseAnnotations parses the annotations contained in the ingress
@ -40,5 +59,14 @@ func (a sslpt) Parse(ing *networking.Ingress) (interface{}, error) {
return false, ing_errors.ErrMissingAnnotations
}
return parser.GetBoolAnnotation("ssl-passthrough", ing)
return parser.GetBoolAnnotation(sslPassthroughAnnotation, ing, a.annotationConfig.Annotations)
}
func (a sslpt) GetDocumentation() parser.AnnotationFields {
return a.annotationConfig.Annotations
}
func (a sslpt) Validate(anns map[string]string) error {
maxrisk := parser.StringRiskToRisk(a.r.GetSecurityConfiguration().AnnotationsRiskLevel)
return parser.CheckAnnotationRisk(anns, maxrisk, sslPassthroughAnnotations.Annotations)
}

View file

@ -54,7 +54,7 @@ func TestParseAnnotations(t *testing.T) {
}
data := map[string]string{}
data[parser.GetAnnotationWithPrefix("ssl-passthrough")] = "true"
data[parser.GetAnnotationWithPrefix(sslPassthroughAnnotation)] = "true"
ing.SetAnnotations(data)
// test ingress using the annotation without a TLS section
_, err = NewParser(&resolver.Mock{}).Parse(ing)

View file

@ -23,18 +23,47 @@ import (
"k8s.io/ingress-nginx/internal/ingress/resolver"
)
const (
streamSnippetAnnotation = "stream-snippet"
)
var streamSnippetAnnotations = parser.Annotation{
Group: "snippets",
Annotations: parser.AnnotationFields{
streamSnippetAnnotation: {
Validator: parser.ValidateNull,
Scope: parser.AnnotationScopeIngress,
Risk: parser.AnnotationRiskCritical, // Critical, this annotation is not validated at all and allows arbitrary configutations
Documentation: `This annotation allows setting a custom NGINX configuration on a stream block. This annotation does not contain any validation and it's usage is not recommended!`,
},
},
}
type streamSnippet struct {
r resolver.Resolver
r resolver.Resolver
annotationConfig parser.Annotation
}
// NewParser creates a new server snippet annotation parser
func NewParser(r resolver.Resolver) parser.IngressAnnotation {
return streamSnippet{r}
return streamSnippet{
r: r,
annotationConfig: streamSnippetAnnotations,
}
}
// Parse parses the annotations contained in the ingress rule
// used to indicate if the location/s contains a fragment of
// configuration to be included inside the paths of the rules
func (a streamSnippet) Parse(ing *networking.Ingress) (interface{}, error) {
return parser.GetStringAnnotation("stream-snippet", ing)
return parser.GetStringAnnotation("stream-snippet", ing, a.annotationConfig.Annotations)
}
func (a streamSnippet) GetDocumentation() parser.AnnotationFields {
return a.annotationConfig.Annotations
}
func (a streamSnippet) Validate(anns map[string]string) error {
maxrisk := parser.StringRiskToRisk(a.r.GetSecurityConfiguration().AnnotationsRiskLevel)
return parser.CheckAnnotationRisk(anns, maxrisk, streamSnippetAnnotations.Annotations)
}

View file

@ -27,7 +27,7 @@ import (
)
func TestParse(t *testing.T) {
annotation := parser.GetAnnotationWithPrefix("stream-snippet")
annotation := parser.GetAnnotationWithPrefix(streamSnippetAnnotation)
ap := NewParser(&resolver.Mock{})
if ap == nil {

View file

@ -17,14 +17,54 @@ limitations under the License.
package upstreamhashby
import (
"regexp"
networking "k8s.io/api/networking/v1"
"k8s.io/ingress-nginx/internal/ingress/annotations/parser"
"k8s.io/ingress-nginx/internal/ingress/errors"
"k8s.io/ingress-nginx/internal/ingress/resolver"
)
const (
upstreamHashByAnnotation = "upstream-hash-by"
upstreamHashBySubsetAnnotation = "upstream-hash-by-subset"
upstreamHashBySubsetSize = "upstream-hash-by-subset-size"
)
var (
specialChars = regexp.QuoteMeta("_${}")
hashByRegex = regexp.MustCompilePOSIX(`^[A-Za-z0-9\-` + specialChars + `]*$`)
)
var upstreamHashByAnnotations = parser.Annotation{
Group: "backend",
Annotations: parser.AnnotationFields{
upstreamHashByAnnotation: {
Validator: parser.ValidateRegex(*hashByRegex, true),
Scope: parser.AnnotationScopeLocation,
Risk: parser.AnnotationRiskHigh, // High, this annotation allows accessing NGINX variables
Documentation: `This annotation defines the nginx variable, text value or any combination thereof to use for consistent hashing.
For example: nginx.ingress.kubernetes.io/upstream-hash-by: "$request_uri" or nginx.ingress.kubernetes.io/upstream-hash-by: "$request_uri$host" or nginx.ingress.kubernetes.io/upstream-hash-by: "${request_uri}-text-value" to consistently hash upstream requests by the current request URI.`,
},
upstreamHashBySubsetAnnotation: {
Validator: parser.ValidateBool,
Scope: parser.AnnotationScopeLocation,
Risk: parser.AnnotationRiskLow,
Documentation: `This annotation maps requests to subset of nodes instead of a single one.`,
},
upstreamHashBySubsetSize: {
Validator: parser.ValidateInt,
Scope: parser.AnnotationScopeLocation,
Risk: parser.AnnotationRiskLow,
Documentation: `This annotation determines the size of each subset (default 3)`,
},
},
}
type upstreamhashby struct {
r resolver.Resolver
r resolver.Resolver
annotationConfig parser.Annotation
}
// Config contains the Consistent hash configuration to be used in the Ingress
@ -36,14 +76,26 @@ type Config struct {
// NewParser creates a new UpstreamHashBy annotation parser
func NewParser(r resolver.Resolver) parser.IngressAnnotation {
return upstreamhashby{r}
return upstreamhashby{
r: r,
annotationConfig: upstreamHashByAnnotations,
}
}
// Parse parses the annotations contained in the ingress rule
func (a upstreamhashby) Parse(ing *networking.Ingress) (interface{}, error) {
upstreamHashBy, _ := parser.GetStringAnnotation("upstream-hash-by", ing)
upstreamHashBySubset, _ := parser.GetBoolAnnotation("upstream-hash-by-subset", ing)
upstreamHashbySubsetSize, _ := parser.GetIntAnnotation("upstream-hash-by-subset-size", ing)
upstreamHashBy, err := parser.GetStringAnnotation(upstreamHashByAnnotation, ing, a.annotationConfig.Annotations)
if err != nil && !errors.IsMissingAnnotations(err) {
return nil, err
}
upstreamHashBySubset, err := parser.GetBoolAnnotation(upstreamHashBySubsetAnnotation, ing, a.annotationConfig.Annotations)
if err != nil && !errors.IsMissingAnnotations(err) {
return nil, err
}
upstreamHashbySubsetSize, err := parser.GetIntAnnotation(upstreamHashBySubsetSize, ing, a.annotationConfig.Annotations)
if err != nil && !errors.IsMissingAnnotations(err) {
return nil, err
}
if upstreamHashbySubsetSize == 0 {
upstreamHashbySubsetSize = 3
@ -51,3 +103,12 @@ func (a upstreamhashby) Parse(ing *networking.Ingress) (interface{}, error) {
return &Config{upstreamHashBy, upstreamHashBySubset, upstreamHashbySubsetSize}, nil
}
func (a upstreamhashby) GetDocumentation() parser.AnnotationFields {
return a.annotationConfig.Annotations
}
func (a upstreamhashby) Validate(anns map[string]string) error {
maxrisk := parser.StringRiskToRisk(a.r.GetSecurityConfiguration().AnnotationsRiskLevel)
return parser.CheckAnnotationRisk(anns, maxrisk, upstreamHashByAnnotations.Annotations)
}

View file

@ -27,7 +27,7 @@ import (
)
func TestParse(t *testing.T) {
annotation := parser.GetAnnotationWithPrefix("upstream-hash-by")
annotation := parser.GetAnnotationWithPrefix(upstreamHashByAnnotation)
ap := NewParser(&resolver.Mock{})
if ap == nil {
@ -37,12 +37,15 @@ func TestParse(t *testing.T) {
testCases := []struct {
annotations map[string]string
expected string
expectErr bool
}{
{map[string]string{annotation: "$request_uri"}, "$request_uri"},
{map[string]string{annotation: "$request_uri$scheme"}, "$request_uri$scheme"},
{map[string]string{annotation: "false"}, "false"},
{map[string]string{}, ""},
{nil, ""},
{map[string]string{annotation: "$request_URI"}, "$request_URI", false},
{map[string]string{annotation: "$request_uri$scheme"}, "$request_uri$scheme", false},
{map[string]string{annotation: "xpto;[]"}, "", true},
{map[string]string{annotation: "lalal${scheme_test}"}, "lalal${scheme_test}", false},
{map[string]string{annotation: "false"}, "false", false},
{map[string]string{}, "", false},
{nil, "", false},
}
ing := &networking.Ingress{
@ -55,14 +58,19 @@ func TestParse(t *testing.T) {
for _, testCase := range testCases {
ing.SetAnnotations(testCase.annotations)
result, _ := ap.Parse(ing)
uc, ok := result.(*Config)
if !ok {
t.Fatalf("expected a Config type")
result, err := ap.Parse(ing)
if (err != nil) != testCase.expectErr {
t.Fatalf("expected error: %t got error: %t err value: %s. %+v", testCase.expectErr, err != nil, err, testCase.annotations)
}
if !testCase.expectErr {
uc, ok := result.(*Config)
if !ok {
t.Fatalf("expected a Config type")
}
if uc.UpstreamHashBy != testCase.expected {
t.Errorf("expected %v but returned %v, annotations: %s", testCase.expected, result, testCase.annotations)
if uc.UpstreamHashBy != testCase.expected {
t.Errorf("expected %v but returned %v, annotations: %s", testCase.expected, result, testCase.annotations)
}
}
}
}

View file

@ -23,18 +23,48 @@ import (
"k8s.io/ingress-nginx/internal/ingress/resolver"
)
const (
upstreamVhostAnnotation = "upstream-vhost"
)
var upstreamVhostAnnotations = parser.Annotation{
Group: "backend",
Annotations: parser.AnnotationFields{
upstreamVhostAnnotation: {
Validator: parser.ValidateServerName,
Scope: parser.AnnotationScopeLocation,
Risk: parser.AnnotationRiskLow, // Low, as it allows regexes but on a very limited set
Documentation: `This configuration setting allows you to control the value for host in the following statement: proxy_set_header Host $host, which forms part of the location block.
This is useful if you need to call the upstream server by something other than $host`,
},
},
}
type upstreamVhost struct {
r resolver.Resolver
r resolver.Resolver
annotationConfig parser.Annotation
}
// NewParser creates a new upstream VHost annotation parser
func NewParser(r resolver.Resolver) parser.IngressAnnotation {
return upstreamVhost{r}
return upstreamVhost{
r: r,
annotationConfig: upstreamVhostAnnotations,
}
}
// Parse parses the annotations contained in the ingress rule
// used to indicate if the location/s contains a fragment of
// configuration to be included inside the paths of the rules
func (a upstreamVhost) Parse(ing *networking.Ingress) (interface{}, error) {
return parser.GetStringAnnotation("upstream-vhost", ing)
return parser.GetStringAnnotation(upstreamVhostAnnotation, ing, a.annotationConfig.Annotations)
}
func (a upstreamVhost) GetDocumentation() parser.AnnotationFields {
return a.annotationConfig.Annotations
}
func (a upstreamVhost) Validate(anns map[string]string) error {
maxrisk := parser.StringRiskToRisk(a.r.GetSecurityConfiguration().AnnotationsRiskLevel)
return parser.CheckAnnotationRisk(anns, maxrisk, upstreamVhostAnnotations.Annotations)
}

View file

@ -36,7 +36,7 @@ func TestParse(t *testing.T) {
}
data := map[string]string{}
data[parser.GetAnnotationWithPrefix("upstream-vhost")] = "ok.com"
data[parser.GetAnnotationWithPrefix(upstreamVhostAnnotation)] = "ok.com"
ing.SetAnnotations(data)

View file

@ -23,17 +23,46 @@ import (
"k8s.io/ingress-nginx/internal/ingress/resolver"
)
const (
xForwardedForPrefixAnnotation = "x-forwarded-prefix"
)
var xForwardedForAnnotations = parser.Annotation{
Group: "backend",
Annotations: parser.AnnotationFields{
xForwardedForPrefixAnnotation: {
Validator: parser.ValidateRegex(*parser.BasicCharsRegex, true),
Scope: parser.AnnotationScopeLocation,
Risk: parser.AnnotationRiskLow, // Low, as it allows regexes but on a very limited set
Documentation: `This annotation can be used to add the non-standard X-Forwarded-Prefix header to the upstream request with a string value`,
},
},
}
type xforwardedprefix struct {
r resolver.Resolver
r resolver.Resolver
annotationConfig parser.Annotation
}
// NewParser creates a new xforwardedprefix annotation parser
func NewParser(r resolver.Resolver) parser.IngressAnnotation {
return xforwardedprefix{r}
return xforwardedprefix{
r: r,
annotationConfig: xForwardedForAnnotations,
}
}
// Parse parses the annotations contained in the ingress rule
// used to add an x-forwarded-prefix header to the request
func (cbbs xforwardedprefix) Parse(ing *networking.Ingress) (interface{}, error) {
return parser.GetStringAnnotation("x-forwarded-prefix", ing)
return parser.GetStringAnnotation(xForwardedForPrefixAnnotation, ing, cbbs.annotationConfig.Annotations)
}
func (cbbs xforwardedprefix) GetDocumentation() parser.AnnotationFields {
return cbbs.annotationConfig.Annotations
}
func (a xforwardedprefix) Validate(anns map[string]string) error {
maxrisk := parser.StringRiskToRisk(a.r.GetSecurityConfiguration().AnnotationsRiskLevel)
return parser.CheckAnnotationRisk(anns, maxrisk, xForwardedForAnnotations.Annotations)
}

View file

@ -27,7 +27,7 @@ import (
)
func TestParse(t *testing.T) {
annotation := parser.GetAnnotationWithPrefix("x-forwarded-prefix")
annotation := parser.GetAnnotationWithPrefix(xForwardedForPrefixAnnotation)
ap := NewParser(&resolver.Mock{})
if ap == nil {
t.Fatalf("expected a parser.IngressAnnotation but returned nil")

View file

@ -97,6 +97,17 @@ type Configuration struct {
// If disabled, only snippets added via ConfigMap are added to ingress.
AllowSnippetAnnotations bool `json:"allow-snippet-annotations"`
// AllowCrossNamespaceResources enables users to consume cross namespace resource on annotations
// Case disabled, attempts to use secrets or configmaps from a namespace different from Ingress will
// be denied
// This value will default to `false` on future releases
AllowCrossNamespaceResources bool `json:"allow-cross-namespace-resources"`
// AnnotationsRiskLevel represents the risk accepted on an annotation. If the risk is, for instance `Medium`, annotations
// with risk High and Critical will not be accepted.
// Default Risk is Critical by default, but this may be changed in future releases
AnnotationsRiskLevel string `json:"annotations-risk-level"`
// AnnotationValueWordBlocklist defines words that should not be part of an user annotation value
// (can be used to run arbitrary code or configs, for example) and that should be dropped.
// This list should be separated by "," character
@ -708,7 +719,7 @@ type Configuration struct {
// DatadogSampleRate specifies sample rate for any traces created.
// Default: use a dynamic rate instead
DatadogSampleRate *float32 `json:"datadog-sample-rate",omitempty`
DatadogSampleRate *float32 `json:"datadog-sample-rate,omitempty"`
// MainSnippet adds custom configuration to the main section of the nginx configuration
MainSnippet string `json:"main-snippet"`
@ -853,8 +864,10 @@ func NewDefault() Configuration {
cfg := Configuration{
AllowSnippetAnnotations: true,
AllowCrossNamespaceResources: true,
AllowBackendServerHeader: false,
AnnotationValueWordBlocklist: "",
AnnotationsRiskLevel: "Critical",
AccessLogPath: "/var/log/nginx/access.log",
AccessLogParams: "",
EnableAccessLogForDefaultBackend: false,

View file

@ -389,14 +389,19 @@ func (n *NGINXController) CheckIngress(ing *networking.Ingress) error {
toCheck.ObjectMeta.Name == ing.ObjectMeta.Name
}
ings := store.FilterIngresses(allIngresses, filter)
parsed, err := annotations.NewAnnotationExtractor(n.store).Extract(ing)
if err != nil {
n.metricCollector.IncCheckErrorCount(ing.ObjectMeta.Namespace, ing.Name)
return err
}
ings = append(ings, &ingress.Ingress{
Ingress: *ing,
ParsedAnnotations: annotations.NewAnnotationExtractor(n.store).Extract(ing),
ParsedAnnotations: parsed,
})
startTest := time.Now().UnixNano() / 1000000
_, servers, pcfg := n.getConfiguration(ings)
err := checkOverlap(ing, allIngresses, servers)
err = checkOverlap(ing, allIngresses, servers)
if err != nil {
n.metricCollector.IncCheckErrorCount(ing.ObjectMeta.Namespace, ing.Name)
return err
@ -1509,7 +1514,7 @@ func locationApplyAnnotations(loc *ingress.Location, anns *annotations.Ingress)
loc.Rewrite = anns.Rewrite
loc.UpstreamVhost = anns.UpstreamVhost
loc.Denylist = anns.Denylist
loc.Whitelist = anns.Whitelist
loc.Allowlist = anns.Allowlist
loc.Denied = anns.Denied
loc.XForwardedPrefix = anns.XForwardedPrefix
loc.UsePortInRedirects = anns.UsePortInRedirects
@ -1808,9 +1813,9 @@ func checkOverlap(ing *networking.Ingress, ingresses []*ingress.Ingress, servers
}
// path overlap. Check if one of the ingresses has a canary annotation
isCanaryEnabled, annotationErr := parser.GetBoolAnnotation("canary", ing)
isCanaryEnabled, annotationErr := parser.GetBoolAnnotation("canary", ing, canary.CanaryAnnotations.Annotations)
for _, existing := range existingIngresses {
isExistingCanaryEnabled, existingAnnotationErr := parser.GetBoolAnnotation("canary", existing)
isExistingCanaryEnabled, existingAnnotationErr := parser.GetBoolAnnotation("canary", existing, canary.CanaryAnnotations.Annotations)
if isCanaryEnabled && isExistingCanaryEnabled {
return fmt.Errorf(`host "%s" and path "%s" is already defined in ingress %s/%s`, rule.Host, path.Path, existing.Namespace, existing.Name)

View file

@ -44,7 +44,7 @@ import (
"k8s.io/ingress-nginx/internal/ingress/annotations"
"k8s.io/ingress-nginx/internal/ingress/annotations/canary"
"k8s.io/ingress-nginx/internal/ingress/annotations/ipwhitelist"
"k8s.io/ingress-nginx/internal/ingress/annotations/ipallowlist"
"k8s.io/ingress-nginx/internal/ingress/annotations/parser"
"k8s.io/ingress-nginx/internal/ingress/annotations/proxyssl"
"k8s.io/ingress-nginx/internal/ingress/annotations/sessionaffinity"
@ -73,6 +73,13 @@ func (fis fakeIngressStore) GetBackendConfiguration() ngx_config.Configuration {
return fis.configuration
}
func (fis fakeIngressStore) GetSecurityConfiguration() defaults.SecurityConfiguration {
return defaults.SecurityConfiguration{
AnnotationsRiskLevel: fis.configuration.AnnotationsRiskLevel,
AllowCrossNamespaceResources: fis.configuration.AllowCrossNamespaceResources,
}
}
func (fakeIngressStore) GetConfigMap(key string) (*corev1.ConfigMap, error) {
return nil, fmt.Errorf("test error")
}
@ -2418,7 +2425,7 @@ func TestGetBackendServers(t *testing.T) {
},
},
ParsedAnnotations: &annotations.Ingress{
Whitelist: ipwhitelist.SourceRange{CIDR: []string{"10.0.0.0/24"}},
Allowlist: ipallowlist.SourceRange{CIDR: []string{"10.0.0.0/24"}},
ServerSnippet: "bla",
ConfigurationSnippet: "blo",
},
@ -2439,7 +2446,7 @@ func TestGetBackendServers(t *testing.T) {
t.Errorf("config snippet should be empty, got '%s'", s.Locations[0].ConfigurationSnippet)
}
if len(s.Locations[0].Whitelist.CIDR) != 1 || s.Locations[0].Whitelist.CIDR[0] != "10.0.0.0/24" {
if len(s.Locations[0].Allowlist.CIDR) != 1 || s.Locations[0].Allowlist.CIDR[0] != "10.0.0.0/24" {
t.Errorf("allow list was incorrectly dropped, len should be 1 and contain 10.0.0.0/24")
}

View file

@ -69,6 +69,9 @@ type Storer interface {
// GetBackendConfiguration returns the nginx configuration stored in a configmap
GetBackendConfiguration() ngx_config.Configuration
// GetSecurityConfiguration returns the configuration options from Ingress
GetSecurityConfiguration() defaults.SecurityConfiguration
// GetConfigMap returns the ConfigMap matching key.
GetConfigMap(key string) (*corev1.ConfigMap, error)
@ -882,9 +885,14 @@ func (s *k8sStore) syncIngress(ing *networkingv1.Ingress) {
k8s.SetDefaultNGINXPathType(copyIng)
err := s.listers.IngressWithAnnotation.Update(&ingress.Ingress{
parsed, err := s.annotations.Extract(ing)
if err != nil {
klog.Error(err)
return
}
err = s.listers.IngressWithAnnotation.Update(&ingress.Ingress{
Ingress: *copyIng,
ParsedAnnotations: s.annotations.Extract(ing),
ParsedAnnotations: parsed,
})
if err != nil {
klog.Error(err)
@ -920,8 +928,10 @@ func (s *k8sStore) updateSecretIngressMap(ing *networkingv1.Ingress) {
"proxy-ssl-secret",
"secure-verify-ca-secret",
}
secConfig := s.GetSecurityConfiguration().AllowCrossNamespaceResources
for _, ann := range secretAnnotations {
secrKey, err := objectRefAnnotationNsKey(ann, ing)
secrKey, err := objectRefAnnotationNsKey(ann, ing, secConfig)
if err != nil && !errors.IsMissingAnnotations(err) {
klog.Errorf("error reading secret reference in annotation %q: %s", ann, err)
continue
@ -937,8 +947,9 @@ func (s *k8sStore) updateSecretIngressMap(ing *networkingv1.Ingress) {
// objectRefAnnotationNsKey returns an object reference formatted as a
// 'namespace/name' key from the given annotation name.
func objectRefAnnotationNsKey(ann string, ing *networkingv1.Ingress) (string, error) {
annValue, err := parser.GetStringAnnotation(ann, ing)
func objectRefAnnotationNsKey(ann string, ing *networkingv1.Ingress, allowCrossNamespace bool) (string, error) {
// We pass nil fields, as this is an internal process and we don't need to validate it.
annValue, err := parser.GetStringAnnotation(ann, ing, nil)
if err != nil {
return "", err
}
@ -951,6 +962,9 @@ func objectRefAnnotationNsKey(ann string, ing *networkingv1.Ingress) (string, er
if secrNs == "" {
return fmt.Sprintf("%v/%v", ing.Namespace, secrName), nil
}
if !allowCrossNamespace && secrNs != ing.Namespace {
return "", fmt.Errorf("cross namespace secret is not supported")
}
return annValue, nil
}
@ -1125,6 +1139,17 @@ func (s *k8sStore) GetBackendConfiguration() ngx_config.Configuration {
return s.backendConfig
}
func (s *k8sStore) GetSecurityConfiguration() defaults.SecurityConfiguration {
s.backendConfigMu.RLock()
defer s.backendConfigMu.RUnlock()
secConfig := defaults.SecurityConfiguration{
AllowCrossNamespaceResources: s.backendConfig.AllowCrossNamespaceResources,
AnnotationsRiskLevel: s.backendConfig.AnnotationsRiskLevel,
}
return secConfig
}
func (s *k8sStore) setConfig(cmap *corev1.ConfigMap) {
s.backendConfigMu.Lock()
defer s.backendConfigMu.Unlock()

View file

@ -1414,18 +1414,31 @@ func TestUpdateSecretIngressMap(t *testing.T) {
t.Run("with annotation in namespace/name format", func(t *testing.T) {
ing := ingTpl.DeepCopy()
ing.ObjectMeta.SetAnnotations(map[string]string{
parser.GetAnnotationWithPrefix("auth-secret"): "otherns/auth",
parser.GetAnnotationWithPrefix("auth-secret"): "testns/auth",
})
if err := s.listers.Ingress.Update(ing); err != nil {
t.Errorf("error updating the Ingress: %v", err)
}
s.updateSecretIngressMap(ing)
if l := s.secretIngressMap.Len(); !(l == 1 && s.secretIngressMap.Has("otherns/auth")) {
if l := s.secretIngressMap.Len(); !(l == 1 && s.secretIngressMap.Has("testns/auth")) {
t.Errorf("Expected \"otherns/auth\" to be the only referenced Secret (got %d)", l)
}
})
t.Run("with annotation in namespace/name format should not be supported", func(t *testing.T) {
ing := ingTpl.DeepCopy()
ing.ObjectMeta.SetAnnotations(map[string]string{
parser.GetAnnotationWithPrefix("auth-secret"): "anotherns/auth",
})
s.listers.Ingress.Update(ing)
s.updateSecretIngressMap(ing)
if l := s.secretIngressMap.Len(); l != 0 {
t.Errorf("Expected \"otherns/auth\" to be denied as it contains a different namespace (got %d)", l)
}
})
t.Run("with annotation in invalid format", func(t *testing.T) {
ing := ingTpl.DeepCopy()
ing.ObjectMeta.SetAnnotations(map[string]string{

View file

@ -170,3 +170,15 @@ type Backend struct {
// It disables that behavior and instead uses a single upstream in NGINX, the service's Cluster IP and port.
ServiceUpstream bool `json:"service-upstream"`
}
type SecurityConfiguration struct {
// AllowCrossNamespaceResources enables users to consume cross namespace resource on annotations
// Case disabled, attempts to use secrets or configmaps from a namespace different from Ingress will
// be denied
// This valid will default to `false` on future releases
AllowCrossNamespaceResources bool `json:"allow-cross-namespace-resources"`
// AnnotationsRiskLevel represents the risk accepted on an annotation. If the risk is, for instance `Medium`, annotations
// with risk High and Critical will not be accepted
AnnotationsRiskLevel string `json:"annotations-risk-level"`
}

View file

@ -110,3 +110,47 @@ func New(m string) error {
func Errorf(format string, args ...interface{}) error {
return fmt.Errorf(format, args...)
}
type ValidationError struct {
Reason error
}
type RiskyAnnotationError struct {
Reason error
}
func (e ValidationError) Error() string {
return e.Reason.Error()
}
// NewValidationError returns a new LocationDenied error
func NewValidationError(annotation string) error {
return ValidationError{
Reason: fmt.Errorf("annotation %s contains invalid value", annotation),
}
}
// IsValidationError checks if the err is an error which
// indicates that some annotation value is invalid
func IsValidationError(e error) bool {
_, ok := e.(ValidationError)
return ok
}
// NewValidationError returns a new LocationDenied error
func NewRiskyAnnotations(name string) error {
return RiskyAnnotationError{
Reason: fmt.Errorf("annotation group %s contains risky annotation based on ingress configuration", name),
}
}
// IsRiskyAnnotationError checks if the err is an error which
// indicates that some annotation value is invalid
func IsRiskyAnnotationError(e error) bool {
_, ok := e.(ValidationError)
return ok
}
func (e RiskyAnnotationError) Error() string {
return e.Reason.Error()
}

View file

@ -26,6 +26,9 @@ type Resolver interface {
// GetDefaultBackend returns the backend that must be used as default
GetDefaultBackend() defaults.Backend
// GetSecurityConfiguration returns the configuration options from Ingress
GetSecurityConfiguration() defaults.SecurityConfiguration
// GetConfigMap searches for configmap containing the namespace and name usting the character /
GetConfigMap(string) (*apiv1.ConfigMap, error)

View file

@ -26,7 +26,9 @@ import (
// Mock implements the Resolver interface
type Mock struct {
ConfigMaps map[string]*apiv1.ConfigMap
ConfigMaps map[string]*apiv1.ConfigMap
AnnotationsRiskLevel string
AllowCrossNamespace bool
}
// GetDefaultBackend returns the backend that must be used as default
@ -34,6 +36,17 @@ func (m Mock) GetDefaultBackend() defaults.Backend {
return defaults.Backend{}
}
func (m Mock) GetSecurityConfiguration() defaults.SecurityConfiguration {
defRisk := m.AnnotationsRiskLevel
if defRisk == "" {
defRisk = "Critical"
}
return defaults.SecurityConfiguration{
AnnotationsRiskLevel: defRisk,
AllowCrossNamespaceResources: m.AllowCrossNamespace,
}
}
// GetSecret searches for secrets contenating the namespace and name using a the character /
func (m Mock) GetSecret(string) (*apiv1.Secret, error) {
return nil, nil

View file

@ -29,8 +29,8 @@ import (
"k8s.io/ingress-nginx/internal/ingress/annotations/cors"
"k8s.io/ingress-nginx/internal/ingress/annotations/fastcgi"
"k8s.io/ingress-nginx/internal/ingress/annotations/globalratelimit"
"k8s.io/ingress-nginx/internal/ingress/annotations/ipallowlist"
"k8s.io/ingress-nginx/internal/ingress/annotations/ipdenylist"
"k8s.io/ingress-nginx/internal/ingress/annotations/ipwhitelist"
"k8s.io/ingress-nginx/internal/ingress/annotations/log"
"k8s.io/ingress-nginx/internal/ingress/annotations/mirror"
"k8s.io/ingress-nginx/internal/ingress/annotations/modsecurity"
@ -224,7 +224,7 @@ type Server struct {
// is required.
// The chain in the execution order of annotations should be:
// - Denylist
// - Whitelist
// - Allowlist
// - RateLimit
// - BasicDigestAuth
// - ExternalAuth
@ -298,10 +298,10 @@ type Location struct {
// addresses or networks are allowed.
// +optional
Denylist ipdenylist.SourceRange `json:"denylist,omitempty"`
// Whitelist indicates only connections from certain client
// Allowlist indicates only connections from certain client
// addresses or networks are allowed.
// +optional
Whitelist ipwhitelist.SourceRange `json:"whitelist,omitempty"`
Allowlist ipallowlist.SourceRange `json:"allowlist,omitempty"`
// Proxy contains information about timeouts and buffer sizes
// to be used in connections against endpoints
// +optional

View file

@ -400,7 +400,7 @@ func (l1 *Location) Equal(l2 *Location) bool {
if !(&l1.Denylist).Equal(&l2.Denylist) {
return false
}
if !(&l1.Whitelist).Equal(&l2.Whitelist) {
if !(&l1.Allowlist).Equal(&l2.Allowlist) {
return false
}
if !(&l1.Proxy).Equal(&l2.Proxy) {

View file

@ -152,6 +152,9 @@ Requires the update-status parameter.`)
annotationsPrefix = flags.String("annotations-prefix", parser.DefaultAnnotationsPrefix,
`Prefix of the Ingress annotations specific to the NGINX controller.`)
enableAnnotationValidation = flags.Bool("enable-annotation-validation", false,
`If true, will enable the annotation validation feature. This value will be defaulted to true on a future release`)
enableSSLChainCompletion = flags.Bool("enable-ssl-chain-completion", false,
`Autocomplete SSL certificate chains with missing intermediate CA certificates.
Certificates uploaded to Kubernetes must have the "Authority Information Access" X.509 v3
@ -249,6 +252,7 @@ https://blog.maxmind.com/2019/12/18/significant-changes-to-accessing-and-using-g
}
parser.AnnotationsPrefix = *annotationsPrefix
parser.EnableAnnotationValidation = *enableAnnotationValidation
// check port collisions
if !ing_net.IsPortAvailable(*httpPort) {

View file

@ -548,14 +548,14 @@ http {
{{ range $rl := (filterRateLimits $servers ) }}
# Ratelimit {{ $rl.Name }}
geo $remote_addr $whitelist_{{ $rl.ID }} {
geo $remote_addr $allowlist_{{ $rl.ID }} {
default 0;
{{ range $ip := $rl.Whitelist }}
{{ range $ip := $rl.Allowlist }}
{{ $ip }} 1;{{ end }}
}
# Ratelimit {{ $rl.Name }}
map $whitelist_{{ $rl.ID }} $limit_{{ $rl.ID }} {
map $allowlist_{{ $rl.ID }} $limit_{{ $rl.ID }} {
0 {{ $cfg.LimitConnZoneVariable }};
1 "";
}
@ -1312,8 +1312,8 @@ stream {
{{ range $ip := $location.Denylist.CIDR }}
deny {{ $ip }};{{ end }}
{{ end }}
{{ if gt (len $location.Whitelist.CIDR) 0 }}
{{ range $ip := $location.Whitelist.CIDR }}
{{ if gt (len $location.Allowlist.CIDR) 0 }}
{{ range $ip := $location.Allowlist.CIDR }}
allow {{ $ip }};{{ end }}
deny all;
{{ end }}

Some files were not shown because too many files have changed in this diff Show more