Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Legacy cherrypick #7965

Merged
merged 3 commits into from
Nov 24, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 7 additions & 10 deletions docs/user-guide/nginx-configuration/configmap.md
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ 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-snippet-annotations](#allow-snippet-annotations)|bool|true|
|[annotation-value-word-blocklist](#annotation-value-word-blocklist)|string array|"load_module","lua_package","_by_lua","location","root","proxy_pass","serviceaccount","{","}","'","\"
|[annotation-value-word-blocklist](#annotation-value-word-blocklist)|string array|""|
|[hide-headers](#hide-headers)|string array|empty|
|[access-log-params](#access-log-params)|string|""|
|[access-log-path](#access-log-path)|string|"/var/log/nginx/access.log"|
Expand Down Expand Up @@ -221,20 +221,17 @@ may allow a user to add restricted configurations to the final nginx.conf file
## annotation-value-word-blocklist

Contains a comma-separated value of chars/words that are well known of being used to abuse Ingress configuration
and must be blocked.
and must be blocked. Related to [CVE-2021-25742](https://github.com/kubernetes/ingress-nginx/issues/7837)

When an annotation is detected with a value that matches one of the blocked badwords, the whole Ingress wont be configured.
When an annotation is detected with a value that matches one of the blocked bad words, the whole Ingress won't be configured.

_**default:**_ `"load_module,lua_package,_by_lua,location,root,proxy_pass,serviceaccount,{,},',\"`
_**default:**_ `""`

When doing this, the default blocklist is override, which means that the Ingress admin should add all the words
that should be blocked, here is a suggested block list.

Warning: The default value already contains a sane set of badwords. Some features like mod_security needs characters that are blocked, and it's up to the Ingress admin to remove this characters from the blocklist.
_**suggested:**_ `"load_module,lua_package,_by_lua,location,root,proxy_pass,serviceaccount,{,},',\"`

When doing this, the default blocklist is overrided, which means that the Ingress admin should add all the words
that should be blocked.

If you find some word should not be on the default list, or if you think that we should add more badwords, please
feel free to open an issue with your case!
## hide-headers

Sets additional header that will not be passed from the upstream server to the client response.
Expand Down
18 changes: 1 addition & 17 deletions internal/ingress/controller/config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,6 @@ package config

import (
"strconv"
"strings"
"time"

"k8s.io/klog/v2"
Expand Down Expand Up @@ -759,21 +758,6 @@ func NewDefault() Configuration {
defNginxStatusIpv4Whitelist := make([]string, 0)
defNginxStatusIpv6Whitelist := make([]string, 0)
defResponseHeaders := make([]string, 0)

defAnnotationValueWordBlocklist := []string{
"load_module",
"lua_package",
"_by_lua",
"location",
"root",
"proxy_pass",
"serviceaccount",
"{",
"}",
"'",
"\\",
}

defIPCIDR = append(defIPCIDR, "0.0.0.0/0")
defNginxStatusIpv4Whitelist = append(defNginxStatusIpv4Whitelist, "127.0.0.1")
defNginxStatusIpv6Whitelist = append(defNginxStatusIpv6Whitelist, "::1")
Expand All @@ -784,7 +768,7 @@ func NewDefault() Configuration {

AllowSnippetAnnotations: true,
AllowBackendServerHeader: false,
AnnotationValueWordBlocklist: strings.Join(defAnnotationValueWordBlocklist, ","),
AnnotationValueWordBlocklist: "",
AccessLogPath: "/var/log/nginx/access.log",
AccessLogParams: "",
EnableAccessLogForDefaultBackend: false,
Expand Down
11 changes: 8 additions & 3 deletions internal/ingress/controller/controller.go
Original file line number Diff line number Diff line change
Expand Up @@ -238,7 +238,11 @@ func (n *NGINXController) CheckIngress(ing *networking.Ingress) error {
cfg := n.store.GetBackendConfiguration()
cfg.Resolver = n.resolver

arraybadWords := strings.Split(strings.TrimSpace(cfg.AnnotationValueWordBlocklist), ",")
var arrayBadWords []string

if cfg.AnnotationValueWordBlocklist != "" {
arrayBadWords = strings.Split(strings.TrimSpace(cfg.AnnotationValueWordBlocklist), ",")
}

for key, value := range ing.ObjectMeta.GetAnnotations() {

Expand All @@ -247,8 +251,9 @@ func (n *NGINXController) CheckIngress(ing *networking.Ingress) error {
return fmt.Errorf("This deployment has a custom annotation prefix defined. Use '%s' instead of '%s'", parser.AnnotationsPrefix, parser.DefaultAnnotationsPrefix)
}
}
if strings.HasPrefix(key, fmt.Sprintf("%s/", parser.AnnotationsPrefix)) {
for _, forbiddenvalue := range arraybadWords {

if strings.HasPrefix(key, fmt.Sprintf("%s/", parser.AnnotationsPrefix)) && len(arrayBadWords) != 0 {
for _, forbiddenvalue := range arrayBadWords {
if strings.Contains(value, strings.TrimSpace(forbiddenvalue)) {
return fmt.Errorf("%s annotation contains invalid word %s", key, forbiddenvalue)
}
Expand Down
9 changes: 5 additions & 4 deletions internal/ingress/controller/store/store.go
Original file line number Diff line number Diff line change
Expand Up @@ -655,10 +655,11 @@ func (s *k8sStore) syncIngress(ing *networkingv1beta1.Ingress) {
copyIng := &networkingv1beta1.Ingress{}
ing.ObjectMeta.DeepCopyInto(&copyIng.ObjectMeta)

klog.Errorf("Blocklist: %v", s.backendConfig.AnnotationValueWordBlocklist)
if err := checkBadAnnotationValue(copyIng.Annotations, s.backendConfig.AnnotationValueWordBlocklist); err != nil {
klog.Errorf("skipping ingress %s: %s", key, err)
return
if s.backendConfig.AnnotationValueWordBlocklist != "" {
if err := checkBadAnnotationValue(copyIng.Annotations, s.backendConfig.AnnotationValueWordBlocklist); err != nil {
klog.Warningf("skipping ingress %s: %s", key, err)
return
}
}

ing.Spec.DeepCopyInto(&copyIng.Spec)
Expand Down
39 changes: 28 additions & 11 deletions test/e2e/admission/admission.go
Original file line number Diff line number Diff line change
Expand Up @@ -146,6 +146,34 @@ var _ = framework.IngressNginxDescribe("[Serial] admission controller", func() {
}
})

ginkgo.It("should return an error if there is an invalid value in some annotation", func() {
host := "admission-test"

annotations := map[string]string{
"nginx.ingress.kubernetes.io/connection-proxy-header": "a;}",
}

f.UpdateNginxConfigMapData("annotation-value-word-blocklist", "}")

firstIngress := framework.NewSingleIngress("first-ingress", "/", host, f.Namespace, framework.EchoService, 80, annotations)
_, err := f.KubeClientSet.NetworkingV1beta1().Ingresses(f.Namespace).Create(context.TODO(), firstIngress, metav1.CreateOptions{})
assert.NotNil(ginkgo.GinkgoT(), err, "creating an ingress with invalid annotation value should return an error")
})

ginkgo.It("should return an error if there is a forbidden value in some annotation", func() {
host := "admission-test"

annotations := map[string]string{
"nginx.ingress.kubernetes.io/connection-proxy-header": "set_by_lua",
}

f.UpdateNginxConfigMapData("annotation-value-word-blocklist", "set_by_lua")

firstIngress := framework.NewSingleIngress("first-ingress", "/", host, f.Namespace, framework.EchoService, 80, annotations)
_, err := f.KubeClientSet.NetworkingV1beta1().Ingresses(f.Namespace).Create(context.TODO(), firstIngress, metav1.CreateOptions{})
assert.NotNil(ginkgo.GinkgoT(), err, "creating an ingress with invalid annotation value should return an error")
})

ginkgo.It("should not return an error if the Ingress V1 definition is valid", func() {
if !f.IsIngressV1Ready {
ginkgo.Skip("Test requires Kubernetes v1.19 or higher")
Expand Down Expand Up @@ -189,17 +217,6 @@ var _ = framework.IngressNginxDescribe("[Serial] admission controller", func() {
_, err := f.KubeClientSet.NetworkingV1beta1().Ingresses(f.Namespace).Create(context.TODO(), firstIngress, metav1.CreateOptions{})
assert.NotNil(ginkgo.GinkgoT(), err, "creating an ingress with invalid annotation value should return an error")
})

ginkgo.It("should return an error if there is a forbidden value in some annotation", func() {
host := "admission-test"

annotations := map[string]string{
"nginx.ingress.kubernetes.io/connection-proxy-header": "set_by_lua",
}
firstIngress := framework.NewSingleIngress("first-ingress", "/", host, f.Namespace, framework.EchoService, 80, annotations)
_, err := f.KubeClientSet.NetworkingV1beta1().Ingresses(f.Namespace).Create(context.TODO(), firstIngress, metav1.CreateOptions{})
assert.NotNil(ginkgo.GinkgoT(), err, "creating an ingress with invalid annotation value should return an error")
})
})

func uninstallChart(f *framework.Framework) error {
Expand Down
2 changes: 1 addition & 1 deletion test/e2e/framework/util.go
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ const (
Poll = 2 * time.Second

// DefaultTimeout time to wait for operations to complete
DefaultTimeout = 30 * time.Second
DefaultTimeout = 90 * time.Second
)

func nowStamp() string {
Expand Down
57 changes: 34 additions & 23 deletions test/e2e/settings/badannotationvalues.go
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ var _ = framework.DescribeAnnotation("Bad annotation values", func() {
f.NewEchoDeployment()
})

ginkgo.It("should drop an ingress if there is an invalid character in some annotation", func() {
ginkgo.It("[BAD_ANNOTATIONS] should drop an ingress if there is an invalid character in some annotation", func() {
host := "invalid-value-test"

annotations := map[string]string{
Expand All @@ -43,6 +43,8 @@ var _ = framework.DescribeAnnotation("Bad annotation values", func() {

ing := framework.NewSingleIngress(host, "/", host, f.Namespace, framework.EchoService, 80, annotations)
f.UpdateNginxConfigMapData("allow-snippet-annotations", "true")
f.UpdateNginxConfigMapData("annotation-value-word-blocklist", "something_forbidden,otherthing_forbidden,{")

f.EnsureIngress(ing)

f.WaitForNginxServer(host,
Expand All @@ -62,7 +64,7 @@ var _ = framework.DescribeAnnotation("Bad annotation values", func() {
Status(http.StatusNotFound)
})

ginkgo.It("should drop an ingress if there is a forbidden word in some annotation", func() {
ginkgo.It("[BAD_ANNOTATIONS] should drop an ingress if there is a forbidden word in some annotation", func() {
host := "forbidden-value-test"

annotations := map[string]string{
Expand All @@ -75,6 +77,7 @@ var _ = framework.DescribeAnnotation("Bad annotation values", func() {

ing := framework.NewSingleIngress(host, "/", host, f.Namespace, framework.EchoService, 80, annotations)
f.UpdateNginxConfigMapData("allow-snippet-annotations", "true")
f.UpdateNginxConfigMapData("annotation-value-word-blocklist", "something_forbidden,otherthing_forbidden,content_by_lua_block")
// Sleep a while just to guarantee that the configmap is applied
framework.Sleep()
f.EnsureIngress(ing)
Expand All @@ -96,46 +99,59 @@ var _ = framework.DescribeAnnotation("Bad annotation values", func() {
Status(http.StatusNotFound)
})

ginkgo.It("should drop an ingress if there is a custom blocklist config in place and allow others to pass", func() {
host := "custom-forbidden-value-test"

annotations := map[string]string{
"nginx.ingress.kubernetes.io/configuration-snippet": `
# something_forbidden`,
}
ginkgo.It("[BAD_ANNOTATIONS] should allow an ingress if there is a default blocklist config in place", func() {

hostValid := "custom-allowed-value-test"
annotationsValid := map[string]string{
"nginx.ingress.kubernetes.io/configuration-snippet": `
# bla_by_lua`,
}

ing := framework.NewSingleIngress(host, "/", host, f.Namespace, framework.EchoService, 80, annotations)
ingValid := framework.NewSingleIngress(hostValid, "/", hostValid, f.Namespace, framework.EchoService, 80, annotationsValid)
f.UpdateNginxConfigMapData("annotation-value-word-blocklist", "something_forbidden,otherthing_forbidden")

// Sleep a while just to guarantee that the configmap is applied
framework.Sleep()
f.EnsureIngress(ing)
f.EnsureIngress(ingValid)

f.WaitForNginxServer(host,
f.WaitForNginxServer(hostValid,
func(server string) bool {
return !strings.Contains(server, fmt.Sprintf("server_name %s ;", host))
return strings.Contains(server, fmt.Sprintf("server_name %s ;", hostValid))
})

f.WaitForNginxServer(hostValid,
func(server string) bool {
return strings.Contains(server, fmt.Sprintf("server_name %s ;", hostValid))
return strings.Contains(server, "# bla_by_lua")
})

f.HTTPTestClient().
GET("/").
WithHeader("Host", hostValid).
Expect().
Status(http.StatusOK)
})

ginkgo.It("[BAD_ANNOTATIONS] should drop an ingress if there is a custom blocklist config in place and allow others to pass", func() {
host := "custom-forbidden-value-test"

annotations := map[string]string{
"nginx.ingress.kubernetes.io/configuration-snippet": `
# something_forbidden`,
}

ing := framework.NewSingleIngress(host, "/", host, f.Namespace, framework.EchoService, 80, annotations)
f.UpdateNginxConfigMapData("annotation-value-word-blocklist", "something_forbidden,otherthing_forbidden")
// Sleep a while just to guarantee that the configmap is applied
framework.Sleep()
f.EnsureIngress(ing)

f.WaitForNginxServer(host,
func(server string) bool {
return !strings.Contains(server, "# something_forbidden")
return !strings.Contains(server, fmt.Sprintf("server_name %s ;", host))
})

f.WaitForNginxServer(hostValid,
f.WaitForNginxServer(host,
func(server string) bool {
return strings.Contains(server, "# bla_by_lua")
return !strings.Contains(server, "# something_forbidden")
})

f.HTTPTestClient().
Expand All @@ -144,10 +160,5 @@ var _ = framework.DescribeAnnotation("Bad annotation values", func() {
Expect().
Status(http.StatusNotFound)

f.HTTPTestClient().
GET("/").
WithHeader("Host", hostValid).
Expect().
Status(http.StatusOK)
})
})