Add insecure A records for pods (#475)

This commit is contained in:
Chris O'Haver 2017-01-11 16:23:10 -05:00 committed by Miek Gieben
parent b10a4f9075
commit 0ee88d3007
4 changed files with 111 additions and 17 deletions

View file

@ -43,21 +43,36 @@ This is the default kubernetes setup, with everything specified in full:
# Kubernetes data API resync period # Kubernetes data API resync period
# Example values: 60s, 5m, 1h # Example values: 60s, 5m, 1h
resyncperiod 5m resyncperiod 5m
# Use url for k8s API endpoint # Use url for k8s API endpoint
endpoint https://k8sendpoint:8080 endpoint https://k8sendpoint:8080
# The tls cert, key and the CA cert filenames # The tls cert, key and the CA cert filenames
tls cert key cacert tls cert key cacert
# Assemble k8s record names with the template # Assemble k8s record names with the template
template {service}.{namespace}.{type}.{zone} template {service}.{namespace}.{type}.{zone}
# Only expose the k8s namespace "demo" # Only expose the k8s namespace "demo"
namespaces demo namespaces demo
# Only expose the records for kubernetes objects # Only expose the records for kubernetes objects
# that match this label selector. The label # that match this label selector. The label
# selector syntax is described in the kubernetes # selector syntax is described in the kubernetes
# API documentation: http://kubernetes.io/docs/user-guide/labels/ # API documentation: http://kubernetes.io/docs/user-guide/labels/
# Example selector below only exposes objects tagged as # Example selector below only exposes objects tagged as
# "application=nginx" in the staging or qa environments. # "application=nginx" in the staging or qa environments.
labels environment in (staging, qa),application=nginx #labels environment in (staging, qa),application=nginx
# The mode of responding to pod A record requests.
# e.g 1-2-3-4.ns.pod.zone. This option is provided to allow use of
# SSL certs when connecting directly to pods.
# Valid values: disabled, verified, insecure
# disabled: default. ignore pod requests, always returning NXDOMAIN
# insecure: Always return an A record with IP from request (without
# checking k8s). This option is is vulnerable to abuse if
# used maliciously in conjuction with wildcard SSL certs.
pods disabled
} }
# Perform DNS response caching for the coredns.local zone # Perform DNS response caching for the coredns.local zone
# Cache timeout is specified by an integer in seconds # Cache timeout is specified by an integer in seconds
@ -72,6 +87,7 @@ Defaults:
* The `labels` keyword is only used when filtering results based on kubernetes label selector syntax * The `labels` keyword is only used when filtering results based on kubernetes label selector syntax
is required. The label selector syntax is described in the kubernetes API documentation at: is required. The label selector syntax is described in the kubernetes API documentation at:
http://kubernetes.io/docs/user-guide/labels/ http://kubernetes.io/docs/user-guide/labels/
* If the `pods` keyword is omitted, all pod type requests will result in NXDOMAIN
### Template Syntax ### Template Syntax
Record name templates can be constructed using the symbolic elements: Record name templates can be constructed using the symbolic elements:

View file

@ -42,8 +42,14 @@ type Kubernetes struct {
Namespaces []string Namespaces []string
LabelSelector *unversionedapi.LabelSelector LabelSelector *unversionedapi.LabelSelector
Selector *labels.Selector Selector *labels.Selector
PodMode string
} }
const (
PodModeDisabled = "disabled" // default. pod requests are ignored
PodModeInsecure = "insecure" // ALL pod requests are answered without verfying they exist
)
type endpoint struct { type endpoint struct {
addr api.EndpointAddress addr api.EndpointAddress
port api.EndpointPort port api.EndpointPort
@ -57,6 +63,12 @@ type service struct {
endpoints []endpoint endpoints []endpoint
} }
type pod struct {
name string
namespace string
addr string
}
var errNoItems = errors.New("no items found") var errNoItems = errors.New("no items found")
var errNsNotExposed = errors.New("namespace is not exposed") var errNsNotExposed = errors.New("namespace is not exposed")
var errInvalidRequest = errors.New("invalid query name") var errInvalidRequest = errors.New("invalid query name")
@ -221,12 +233,9 @@ func (k *Kubernetes) Records(name string, exact bool) ([]msg.Service, error) {
return nil, errNoItems return nil, errNoItems
} }
// TODO: Implementation above globbed together segments for the serviceName if serviceName = serviceSegments[0]
// multiple segments remained. Determine how to do similar globbing using namespace = serviceSegments[1]
// the template-based implementation. typeName = serviceSegments[2]
namespace = k.NameTemplate.NamespaceFromSegmentArray(serviceSegments)
serviceName = k.NameTemplate.ServiceFromSegmentArray(serviceSegments)
typeName = k.NameTemplate.TypeFromSegmentArray(serviceSegments)
if namespace == "" { if namespace == "" {
err := errors.New("Parsing query string did not produce a namespace value. Assuming wildcard namespace.") err := errors.New("Parsing query string did not produce a namespace value. Assuming wildcard namespace.")
@ -246,16 +255,16 @@ func (k *Kubernetes) Records(name string, exact bool) ([]msg.Service, error) {
return nil, errNsNotExposed return nil, errNsNotExposed
} }
k8sItems, err := k.Get(namespace, serviceName, endpointname, port, protocol, typeName) services, pods, err := k.Get(namespace, serviceName, endpointname, port, protocol, typeName)
if err != nil { if err != nil {
return nil, err return nil, err
} }
if len(k8sItems) == 0 { if len(services) == 0 && len(pods) == 0 {
// Did not find item in k8s // Did not find item in k8s
return nil, errNoItems return nil, errNoItems
} }
records := k.getRecordsForServiceItems(k8sItems, zone) records := k.getRecordsForK8sItems(services, pods, zone)
return records, nil return records, nil
} }
@ -272,10 +281,10 @@ func endpointHostname(addr api.EndpointAddress) string {
return "" return ""
} }
func (k *Kubernetes) getRecordsForServiceItems(serviceItems []service, zone string) []msg.Service { func (k *Kubernetes) getRecordsForK8sItems(services []service, pods []pod, zone string) []msg.Service {
var records []msg.Service var records []msg.Service
for _, svc := range serviceItems { for _, svc := range services {
key := svc.name + "." + svc.namespace + ".svc." + zone key := svc.name + "." + svc.namespace + ".svc." + zone
@ -299,20 +308,61 @@ func (k *Kubernetes) getRecordsForServiceItems(serviceItems []service, zone stri
} }
} }
for _, p := range pods {
key := p.name + "." + p.namespace + ".pod." + zone
s := msg.Service{
Key: msg.Path(strings.ToLower(key), "coredns"),
Host: p.addr,
}
records = append(records, s)
}
return records return records
} }
// Get performs the call to the Kubernetes http API. // Get retrieves matching data from the cache.
func (k *Kubernetes) Get(namespace, servicename, endpointname, port, protocol, typeName string) (services []service, err error) { func (k *Kubernetes) Get(namespace, servicename, endpointname, port, protocol, typeName string) (services []service, pods []pod, err error) {
switch { switch {
case typeName == "pod": case typeName == "pod":
return nil, fmt.Errorf("%v not implemented", typeName) pods, err = k.findPods(namespace, servicename)
return nil, pods, err
default: default:
return k.getServices(namespace, servicename, endpointname, port, protocol) services, err = k.findServices(namespace, servicename, endpointname, port, protocol)
return services, nil, err
} }
} }
func (k *Kubernetes) getServices(namespace, servicename, endpointname, port, protocol string) ([]service, error) { func ipFromPodName(podname string) string {
if strings.Count(podname, "-") == 3 && !strings.Contains(podname, "--") {
return strings.Replace(podname, "-", ".", -1)
}
return strings.Replace(podname, "-", ":", -1)
}
func (k *Kubernetes) findPods(namespace, podname string) (pods []pod, err error) {
if k.PodMode == PodModeDisabled {
return pods, nil
}
var ip string
if strings.Count(podname, "-") == 3 && !strings.Contains(podname, "--") {
ip = strings.Replace(podname, "-", ".", -1)
} else {
ip = strings.Replace(podname, "-", ":", -1)
}
if k.PodMode == PodModeInsecure {
s := pod{name: podname, namespace: namespace, addr: ip}
pods = append(pods, s)
return pods, nil
}
// TODO: implement cache verified pod responses
return pods, nil
}
func (k *Kubernetes) findServices(namespace, servicename, endpointname, port, protocol string) ([]service, error) {
serviceList := k.APIConn.ServiceList() serviceList := k.APIConn.ServiceList()
var resultItems []service var resultItems []service

View file

@ -54,6 +54,7 @@ func kubernetesParse(c *caddy.Controller) (*Kubernetes, error) {
k8s := &Kubernetes{ResyncPeriod: defaultResyncPeriod} k8s := &Kubernetes{ResyncPeriod: defaultResyncPeriod}
k8s.NameTemplate = new(nametemplate.Template) k8s.NameTemplate = new(nametemplate.Template)
k8s.NameTemplate.SetTemplate(defaultNameTemplate) k8s.NameTemplate.SetTemplate(defaultNameTemplate)
k8s.PodMode = PodModeDisabled
for c.Next() { for c.Next() {
if c.Val() == "kubernetes" { if c.Val() == "kubernetes" {
@ -86,6 +87,19 @@ func kubernetesParse(c *caddy.Controller) (*Kubernetes, error) {
for c.NextBlock() { for c.NextBlock() {
switch c.Val() { switch c.Val() {
case "pods":
args := c.RemainingArgs()
if len(args) == 1 {
switch args[0] {
case PodModeDisabled, PodModeInsecure:
k8s.PodMode = args[0]
default:
return nil, errors.New("pods must be one of: disabled, insecure")
}
continue
}
return nil, c.ArgErr()
case "template": case "template":
args := c.RemainingArgs() args := c.RemainingArgs()
if len(args) > 0 { if len(args) > 0 {
@ -152,4 +166,5 @@ func kubernetesParse(c *caddy.Controller) (*Kubernetes, error) {
const ( const (
defaultNameTemplate = "{service}.{namespace}.{type}.{zone}" defaultNameTemplate = "{service}.{namespace}.{type}.{zone}"
defaultResyncPeriod = 5 * time.Minute defaultResyncPeriod = 5 * time.Minute
defaultPodMode = PodModeDisabled
) )

View file

@ -194,6 +194,18 @@ var dnsTestCases = []test.Case{
Rcode: dns.RcodeNameError, Rcode: dns.RcodeNameError,
Answer: []dns.RR{}, Answer: []dns.RR{},
}, },
{
Qname: "10-20-0-101.test-1.pod.cluster.local.", Qtype: dns.TypeA,
Rcode: dns.RcodeSuccess,
Answer: []dns.RR{
test.A("10-20-0-101.test-1.pod.cluster.local. 0 IN A 10.20.0.101"),
},
},
{
Qname: "10-20-0-101.test-X.pod.cluster.local.", Qtype: dns.TypeA,
Rcode: dns.RcodeNameError,
Answer: []dns.RR{},
},
{ {
Qname: "123.0.0.10.in-addr.arpa.", Qtype: dns.TypePTR, Qname: "123.0.0.10.in-addr.arpa.", Qtype: dns.TypePTR,
Rcode: dns.RcodeSuccess, Rcode: dns.RcodeSuccess,
@ -238,6 +250,7 @@ func TestKubernetesIntegration(t *testing.T) {
#tls admin.pem admin-key.pem ca.pem #tls admin.pem admin-key.pem ca.pem
#tls k8s_auth/client2.crt k8s_auth/client2.key k8s_auth/ca2.crt #tls k8s_auth/client2.crt k8s_auth/client2.key k8s_auth/ca2.crt
namespaces test-1 namespaces test-1
pods insecure
} }
` `
server, udp := createTestServer(t, corefile) server, udp := createTestServer(t, corefile)