blob: a1d57a5e1abfa9613919ee29aef4b3d8815022a0 [file] [log] [blame]
Serge Bazanski64956532021-01-30 19:19:32 +01001package main
2
3import (
Serge Bazanski5d2c8fc2021-01-30 21:23:53 +01004 "encoding/json"
Serge Bazanski64956532021-01-30 19:19:32 +01005 "fmt"
6 "strings"
7
Serge Bazanski5d2c8fc2021-01-30 21:23:53 +01008 "github.com/golang/glog"
Serge Bazanski64956532021-01-30 19:19:32 +01009 admission "k8s.io/api/admission/v1beta1"
Serge Bazanski5d2c8fc2021-01-30 21:23:53 +010010 networking "k8s.io/api/networking/v1beta1"
11 meta "k8s.io/apimachinery/pkg/apis/meta/v1"
Serge Bazanski64956532021-01-30 19:19:32 +010012)
13
14// ingressFilter is a filter which allows or denies the creation of an ingress
15// backing a given domain with a namespace. It does so by operating on an
16// explicit list of allowed namespace/domain pairs, where each domain is either
17// a single domain or a DNS wildcard at a given root.
18// By default every domain is allowed in every namespace. However, the moment
19// an entry is added for a given domain (or wildcard that matches some
20// domains), this domain will only be allowed in that namespace.
21//
22// For example, with the given allowed domains:
23// - ns: example, domain: one.example.com
24// - ns: example, domain: *.google.com
25// The logic will be as follows:
26// - one.example.com will be only allowed in the example namespace
27// - any .google.com domain will be only allowed in the example namespace
28// - all other domains will be allowed everywhere.
29//
30// This logic allows for the easy use of arbitrary domains by k8s users within
31// their personal namespaces, but allows critical domains to only be allowed in
32// trusted namespaces.
33//
34// ingressFilter can be used straight away after constructing it as an empty
35// type.
36type ingressFilter struct {
37 // allowed is a map from namespace to list of domain matchers.
38 allowed map[string][]*domain
39}
40
41// domain is a matcher for either a single given domain, or a domain wildcard.
42// If this is a wildcard matcher, any amount of dot-delimited levels under the
43// domain will be permitted.
44type domain struct {
45 // dns is either the domain name matched by this matcher (if wildcard ==
46 // false), or the root of a wildcard represented by this matcher (if
47 // wildcard == true).
48 dns string
49 wildcard bool
50}
51
52// match returns whether this matcher matches a given domain.
53func (d *domain) match(dns string) bool {
54 if !d.wildcard {
55 return dns == d.dns
56 }
57 return strings.HasSuffix(dns, "."+d.dns)
58}
59
60// allow adds a given (namespace, dns) pair to the filter. The dns variable is
61// a string that is either a simple domain name, or a wildcard like
62// *.foo.example.com. An error is returned if the dns stirng could not be
63// parsed.
64func (i *ingressFilter) allow(ns, dns string) error {
65 // If the filter is brand new, initialize it.
66 if i.allowed == nil {
67 i.allowed = make(map[string][]*domain)
68 }
69
70 // Try to parse the name as a wildcard.
71 parts := strings.Split(dns, ".")
72 wildcard := false
73 for i, part := range parts {
74 if i == 0 && part == "*" {
75 wildcard = true
76 continue
77 }
78 // Do some basic validation of the name.
79 if part == "" || strings.Contains(part, "*") {
80 return fmt.Errorf("invalid domain")
81 }
82 }
83 if wildcard {
84 if len(parts) < 2 {
85 return fmt.Errorf("invalid domain")
86 }
87 dns = strings.Join(parts[1:], ".")
88 }
89 i.allowed[ns] = append(i.allowed[ns], &domain{
90 dns: dns,
91 wildcard: wildcard,
92 })
93 return nil
94}
95
96// domainAllowed returns whether a given domain is allowed to be backed by an
97// ingress within a given namespace.
98func (i *ingressFilter) domainAllowed(ns, domain string) bool {
99 if i.allowed == nil {
100 return true
101 }
102
103 domainFound := false
104 // TODO(q3k): if this becomes too slow, build some inverted index for this.
105 for n, ds := range i.allowed {
106 for _, d := range ds {
107 if !d.match(domain) {
108 continue
109 }
110 // Domain matched, see if allowed in this namespace.
111 domainFound = true
112 if n == ns {
113 return true
114 }
115 }
116 // Otherwise, maybe it's allowed in another domain.
117 }
118 // No direct match found - if this domain has been at all matched before,
119 // it means that it's a restriected domain and the requested namespace is
120 // not one that's allowed to host it. Refuse.
121 if domainFound {
122 return false
123 }
124 // No direct match found, and this domain is not restricted. Allow.
125 return true
126}
127
128func (i *ingressFilter) admit(req *admission.AdmissionRequest) (*admission.AdmissionResponse, error) {
129 if req.Kind.Group != "networking.k8s.io" || req.Kind.Kind != "Ingress" {
130 return nil, fmt.Errorf("not an ingress")
131 }
Serge Bazanski5d2c8fc2021-01-30 21:23:53 +0100132
133 result := func(s string, args ...interface{}) (*admission.AdmissionResponse, error) {
134 res := &admission.AdmissionResponse{
135 UID: req.UID,
136 }
137 if s == "" {
138 res.Allowed = true
139 } else {
140 res.Allowed = false
141 res.Result = &meta.Status{
142 Code: 403,
143 Message: fmt.Sprintf("admitomatic: %s", fmt.Sprintf(s, args...)),
144 }
145 }
146 return res, nil
147 }
148
149 // Permit any actions on critical system namespaes. See:
150 // https://kubernetes.io/docs/reference/access-authn-authz/extensible-admission-controllers/
151 // “Avoiding operating on the kube-system namespace”
152 if req.Namespace == "kube-system" {
153 return result("")
154 }
155
156 switch req.Operation {
157 case "CREATE":
158 case "UPDATE":
159 default:
160 // We only care about creations/updates, everything else is referred to plain RBAC.
161 return result("")
162 }
163
164 ingress := networking.Ingress{}
165 err := json.Unmarshal(req.Object.Raw, &ingress)
166 if err != nil {
167 glog.Errorf("Unmarshaling Ingress failed: %v", err)
168 return result("invalid object")
169 }
170
171 // Check TLS config for hosts.
172 for j, t := range ingress.Spec.TLS {
173 for k, h := range t.Hosts {
174 if strings.Contains(h, "*") {
175 // TODO(q3k): support wildcards
176 return result("wildcard host %q (%d in TLS entry %d) is not permitted", h, k, j)
177 }
178 if !i.domainAllowed(req.Namespace, h) {
179 return result("host %q (%d) in TLS entry %d is not allowed in namespace %q", h, k, j, req.Namespace)
180 }
181 }
182 }
183
184 // Check rules for hosts.
185 for j, r := range ingress.Spec.Rules {
186 h := r.Host
187 // Per IngressRule spec:
188 // If the host is unspecified, the Ingress routes all traffic based
189 // on the specified IngressRuleValue. Host can be "precise" which is
190 // a domain name without the terminating dot of a network host (e.g.
191 // "foo.bar.com") or "wildcard", which is a domain name prefixed with
192 // a single wildcard label (e.g. "*.foo.com").
193 //
194 // We reject everything other than precise hosts.
195 if h == "" {
196 return result("empty host %q (in rule %d) is not permitted", h, j)
197 }
198 if strings.Contains(h, "*") {
199 // TODO(q3k): support wildcards
200 return result("wildcard host %q (in rule %d) is not permitted", h, j)
201 }
202 if !i.domainAllowed(req.Namespace, h) {
203 return result("host %q (in rule %d) is not allowed in namespace %q", h, j, req.Namespace)
204 }
205 }
206
207 // Only allow a trusted subset of n-i-c annotations.
208 // TODO(q3k): allow opt-out for some namespaces
209 allowed := map[string]bool{
210 "proxy-body-size": true,
211 "ssl-redirect": true,
212 "backend-protocol": true,
213 }
214 prefix := "nginx.ingress.kubernetes.io/"
215 for k, _ := range ingress.Annotations {
216 if !strings.HasPrefix(k, prefix) {
217 continue
218 }
219 k = strings.TrimPrefix(k, prefix)
220 if !allowed[k] {
221 return result("forbidden annotation %q", k)
222 }
223 }
224
225 // All clear, accept this Ingress.
226 return result("")
Serge Bazanski64956532021-01-30 19:19:32 +0100227}