blob: 42cab98b88df84daa7b90d04dfa83cf8fb0f1b34 [file] [log] [blame]
package main
import (
"fmt"
"strings"
admission "k8s.io/api/admission/v1beta1"
)
// ingressFilter is a filter which allows or denies the creation of an ingress
// backing a given domain with a namespace. It does so by operating on an
// explicit list of allowed namespace/domain pairs, where each domain is either
// a single domain or a DNS wildcard at a given root.
// By default every domain is allowed in every namespace. However, the moment
// an entry is added for a given domain (or wildcard that matches some
// domains), this domain will only be allowed in that namespace.
//
// For example, with the given allowed domains:
// - ns: example, domain: one.example.com
// - ns: example, domain: *.google.com
// The logic will be as follows:
// - one.example.com will be only allowed in the example namespace
// - any .google.com domain will be only allowed in the example namespace
// - all other domains will be allowed everywhere.
//
// This logic allows for the easy use of arbitrary domains by k8s users within
// their personal namespaces, but allows critical domains to only be allowed in
// trusted namespaces.
//
// ingressFilter can be used straight away after constructing it as an empty
// type.
type ingressFilter struct {
// allowed is a map from namespace to list of domain matchers.
allowed map[string][]*domain
}
// domain is a matcher for either a single given domain, or a domain wildcard.
// If this is a wildcard matcher, any amount of dot-delimited levels under the
// domain will be permitted.
type domain struct {
// dns is either the domain name matched by this matcher (if wildcard ==
// false), or the root of a wildcard represented by this matcher (if
// wildcard == true).
dns string
wildcard bool
}
// match returns whether this matcher matches a given domain.
func (d *domain) match(dns string) bool {
if !d.wildcard {
return dns == d.dns
}
return strings.HasSuffix(dns, "."+d.dns)
}
// allow adds a given (namespace, dns) pair to the filter. The dns variable is
// a string that is either a simple domain name, or a wildcard like
// *.foo.example.com. An error is returned if the dns stirng could not be
// parsed.
func (i *ingressFilter) allow(ns, dns string) error {
// If the filter is brand new, initialize it.
if i.allowed == nil {
i.allowed = make(map[string][]*domain)
}
// Try to parse the name as a wildcard.
parts := strings.Split(dns, ".")
wildcard := false
for i, part := range parts {
if i == 0 && part == "*" {
wildcard = true
continue
}
// Do some basic validation of the name.
if part == "" || strings.Contains(part, "*") {
return fmt.Errorf("invalid domain")
}
}
if wildcard {
if len(parts) < 2 {
return fmt.Errorf("invalid domain")
}
dns = strings.Join(parts[1:], ".")
}
i.allowed[ns] = append(i.allowed[ns], &domain{
dns: dns,
wildcard: wildcard,
})
return nil
}
// domainAllowed returns whether a given domain is allowed to be backed by an
// ingress within a given namespace.
func (i *ingressFilter) domainAllowed(ns, domain string) bool {
if i.allowed == nil {
return true
}
domainFound := false
// TODO(q3k): if this becomes too slow, build some inverted index for this.
for n, ds := range i.allowed {
for _, d := range ds {
if !d.match(domain) {
continue
}
// Domain matched, see if allowed in this namespace.
domainFound = true
if n == ns {
return true
}
}
// Otherwise, maybe it's allowed in another domain.
}
// No direct match found - if this domain has been at all matched before,
// it means that it's a restriected domain and the requested namespace is
// not one that's allowed to host it. Refuse.
if domainFound {
return false
}
// No direct match found, and this domain is not restricted. Allow.
return true
}
func (i *ingressFilter) admit(req *admission.AdmissionRequest) (*admission.AdmissionResponse, error) {
if req.Kind.Group != "networking.k8s.io" || req.Kind.Kind != "Ingress" {
return nil, fmt.Errorf("not an ingress")
}
// TODO(q3k); implement
return nil, fmt.Errorf("unimplemented")
}