Skip to content

Commit

Permalink
Merge pull request #915 from davecheney/tls-certificate-delegation
Browse files Browse the repository at this point in the history
internal/dag: implement TLS Certificate Delegation
  • Loading branch information
davecheney committed Mar 4, 2019
2 parents b50f5e8 + 6018cbf commit fb273e6
Show file tree
Hide file tree
Showing 5 changed files with 463 additions and 116 deletions.
2 changes: 1 addition & 1 deletion design/tls-certificate-delegation.md
Expand Up @@ -33,7 +33,7 @@ The implementation of this design is in three parts; the addition of a TLSCertif

### TLSCertificateDelegation CRD

The TLSCertificateDelegation object records the permission to reference a Secret object from the namespace of the TLSCertificateDelegation object to Ingress or IngressRoute objects in the target namespaces.
The TLSCertificateDelegation object records the permission to reference a Secret object from the namespace of the TLSCertificateDelegation object to Ingress or IngressRoute objects in the target namespaces.
This permission is managed by the Ingress controller which has the RBAC permissions to read all the relevant Secrets but currently only allows an Ingress or IngressRoute object to reference secrets from its own namespace.

```
Expand Down
39 changes: 39 additions & 0 deletions docs/ingressroute.md
Expand Up @@ -240,6 +240,9 @@ spec:
port: 80
```
If the `tls.secretName` property contains a slash, eg. `somenamespace/somesecret` then, subject to TLS Certificate Delegation, the TLS certificate will be read from `somesecret` in `somenamespace`.
See TLS Certificate Delegation below for more information.
The TLS **Minimum Protocol Version** a vhost should negotiate can be specified by setting the `spec.virtualhost.tls.minimumProtocolVersion`:
- 1.3
- 1.2
Expand Down Expand Up @@ -270,6 +273,42 @@ spec:
permitInsecure: true
```
#### TLS Certificate Delegation
In order to support wildcard certificates, TLS certificates for a `*.somedomain.com`, which are stored in a namespace controlled by the cluster administrator, Contour supports a facility known as TLS Certificate Delegation.
This facility allows the owner of a TLS certificate to delegate, for the purposes of reference the TLS certificate, the when processing an IngressRoute to Contour will reference the Secret object from another namespace.
```yaml
apiVersion: contour.heptio.com/v1beta1
kind: TLSCertificateDelegation
metadata:
name: example-com-wildcard
namespace: www-admin
spec:
delegations:
secretName: example-com-wildcard
targetNamespaces:
- example-com
---
apiVersion: contour.heptio.com/v1beta1
kind: IngressRoute
metadata:
name: www
namespace: example-com
spec:
virtualhost:
fqdn: foo2.bar.com
tls:
secretName: www-admin/example-com-wildcard
routes:
- match: /
services:
- name: s1
port: 80
```
In this example, the permission for Contour to reference the Secret `example-com-wildcard` in the `admin` namespace has been delegated to IngressRoute objects in the `example-com` namespace.
### Routing
Each route entry in an IngressRoute must start with a prefix match.
Expand Down
305 changes: 190 additions & 115 deletions internal/dag/builder.go
Expand Up @@ -384,121 +384,12 @@ func (b *builder) compute() *DAG {

// setup secure vhosts if there is a matching secret
// we do this first so that the set of active secure vhosts is stable
// during the second ingress pass
for _, ing := range b.source.ingresses {
for _, tls := range ing.Spec.TLS {
m := meta{name: tls.SecretName, namespace: ing.Namespace}
if sec := b.lookupSecret(m); sec != nil {
for _, host := range tls.Hosts {
svhost := b.lookupSecureVirtualHost(host)
svhost.Secret = sec
svhost.MinProtoVersion = minProtoVersion(ing)
}
}
}
}

// deconstruct each ingress into routes and virtualhost entries
for _, ing := range b.source.ingresses {
// rewrite the default ingress to a stock ingress rule.
rules := ing.Spec.Rules
if backend := ing.Spec.Backend; backend != nil {
rule := v1beta1.IngressRule{
IngressRuleValue: v1beta1.IngressRuleValue{
HTTP: &v1beta1.HTTPIngressRuleValue{
Paths: []v1beta1.HTTPIngressPath{{
Backend: v1beta1.IngressBackend{
ServiceName: backend.ServiceName,
ServicePort: backend.ServicePort,
},
}},
},
},
}
rules = append(rules, rule)
}

for _, rule := range rules {
host := rule.Host
if host == "" {
host = "*"
}
for _, httppath := range httppaths(rule) {
prefix := httppath.Path
if prefix == "" {
prefix = "/"
}

r := prefixRoute(ing, prefix)
m := meta{name: httppath.Backend.ServiceName, namespace: ing.Namespace}
if s := b.lookupHTTPService(m, httppath.Backend.ServicePort, 0, "", nil); s != nil {

r.addHTTPService(s)
}

// should we create port 80 routes for this ingress
if httpAllowed(ing) {
b.lookupVirtualHost(host).addRoute(r)
}
if _, ok := b.listener(b.externalSecurePort()).VirtualHosts[host]; ok && host != "*" {
b.lookupSecureVirtualHost(host).addRoute(r)
}
}
}
}
// during computeIngresses.
b.computeSecureVirtualhosts()

// process ingressroute documents
for _, ir := range b.validIngressRoutes() {
if ir.Spec.VirtualHost == nil {
// mark delegate ingressroute orphaned.
b.setOrphaned(ir)
continue
}
b.computeIngresses()

// ensure root ingressroute lives in allowed namespace
if !b.rootAllowed(ir) {
b.setStatus(Status{Object: ir, Status: StatusInvalid, Description: "root IngressRoute cannot be defined in this namespace"})
continue
}

host := ir.Spec.VirtualHost.Fqdn
if isBlank(host) {
b.setStatus(Status{Object: ir, Status: StatusInvalid, Description: "Spec.VirtualHost.Fqdn must be specified"})
continue
}

var enforceTLS, passthrough bool
if tls := ir.Spec.VirtualHost.TLS; tls != nil {
// attach secrets to TLS enabled vhosts
m := meta{name: tls.SecretName, namespace: ir.Namespace}
if sec := b.lookupSecret(m); sec != nil {
svhost := b.lookupSecureVirtualHost(host)
svhost.Secret = sec
enforceTLS = true

// process min protocol version
switch ir.Spec.VirtualHost.TLS.MinimumProtocolVersion {
case "1.3":
svhost.MinProtoVersion = auth.TlsParameters_TLSv1_3
case "1.2":
svhost.MinProtoVersion = auth.TlsParameters_TLSv1_2
default:
// any other value is interpreted as TLS/1.1
svhost.MinProtoVersion = auth.TlsParameters_TLSv1_1
}
}
// passthrough is true if tls.secretName is not present, and
// tls.passthrough is set to true.
passthrough = tls.SecretName == "" && tls.Passthrough
}

switch {
case ir.Spec.TCPProxy != nil && (passthrough || enforceTLS):
b.processTCPProxy(ir, nil, host)
case ir.Spec.Routes != nil:
b.processRoutes(ir, "", nil, host, enforceTLS)
}
}
b.computeIngressRoutes()

return b.DAG()
}
Expand Down Expand Up @@ -532,8 +423,8 @@ func isBlank(s string) bool {

// minProtoVersion returns the TLS protocol version specified by an ingress annotation
// or default if non present.
func minProtoVersion(i *v1beta1.Ingress) auth.TlsParameters_TlsProtocol {
switch i.Annotations["contour.heptio.com/tls-minimum-protocol-version"] {
func minProtoVersion(version string) auth.TlsParameters_TlsProtocol {
switch version {
case "1.3":
return auth.TlsParameters_TLSv1_3
case "1.2":
Expand Down Expand Up @@ -579,6 +470,190 @@ func (b *builder) validIngressRoutes() []*ingressroutev1.IngressRoute {
return valid
}

// computeSecureVirtualhosts populates tls parameters of
// secure virtual hosts.
func (b *builder) computeSecureVirtualhosts() {
for _, ing := range b.source.ingresses {
for _, tls := range ing.Spec.TLS {
m := splitSecret(tls.SecretName, ing.Namespace)
if sec := b.lookupSecret(m); sec != nil && b.delegationPermitted(m, ing.Namespace) {
for _, host := range tls.Hosts {
svhost := b.lookupSecureVirtualHost(host)
svhost.Secret = sec
version := ing.Annotations["contour.heptio.com/tls-minimum-protocol-version"]
svhost.MinProtoVersion = minProtoVersion(version)
}
}
}
}
}

// splitSecret splits a secretName into its namespace and name components.
// If there is no namespace prefix, the default namespace is returned.
func splitSecret(secret, defns string) meta {
v := strings.SplitN(secret, "/", 2)
switch len(v) {
case 1:
// no prefix
return meta{
name: v[0],
namespace: defns,
}
default:
return meta{
name: v[1],
namespace: stringOrDefault(v[0], defns),
}
}
}

func (b *builder) delegationPermitted(secret meta, to string) bool {
contains := func(haystack []string, needle string) bool {
if len(haystack) == 1 && haystack[0] == "*" {
return true
}
for _, h := range haystack {
if h == needle {
return true
}
}
return false
}

if secret.namespace == to {
// secret is in the same namespace as target
return true
}
for _, d := range b.source.delegations {
if d.Namespace != secret.namespace {
continue
}
for _, d := range d.Spec.Delegations {
if contains(d.TargetNamespaces, to) {
if secret.name == d.SecretName {
return true
}
}
}
}
return false
}

func (b *builder) computeIngresses() {
// deconstruct each ingress into routes and virtualhost entries
for _, ing := range b.source.ingresses {

// rewrite the default ingress to a stock ingress rule.
rules := rulesFromSpec(ing.Spec)

for _, rule := range rules {
host := stringOrDefault(rule.Host, "*")
for _, httppath := range httppaths(rule) {
prefix := stringOrDefault(httppath.Path, "/")
r := prefixRoute(ing, prefix)
be := httppath.Backend
m := meta{name: be.ServiceName, namespace: ing.Namespace}
if s := b.lookupHTTPService(m, be.ServicePort, 0, "", nil); s != nil {

r.addHTTPService(s)
}

// should we create port 80 routes for this ingress
if httpAllowed(ing) {
b.lookupVirtualHost(host).addRoute(r)
}

if b.secureVirtualhostExists(host) && host != "*" {
b.lookupSecureVirtualHost(host).addRoute(r)
}
}
}
}
}

func stringOrDefault(s, def string) string {
if s == "" {
return def
}
return s
}

func (b *builder) computeIngressRoutes() {
for _, ir := range b.validIngressRoutes() {
if ir.Spec.VirtualHost == nil {
// mark delegate ingressroute orphaned.
b.setOrphaned(ir)
continue
}

// ensure root ingressroute lives in allowed namespace
if !b.rootAllowed(ir) {
b.setStatus(Status{Object: ir, Status: StatusInvalid, Description: "root IngressRoute cannot be defined in this namespace"})
continue
}

host := ir.Spec.VirtualHost.Fqdn
if isBlank(host) {
b.setStatus(Status{Object: ir, Status: StatusInvalid, Description: "Spec.VirtualHost.Fqdn must be specified"})
continue
}

var enforceTLS, passthrough bool
if tls := ir.Spec.VirtualHost.TLS; tls != nil {
// attach secrets to TLS enabled vhosts
m := splitSecret(tls.SecretName, ir.Namespace)
if sec := b.lookupSecret(m); sec != nil && b.delegationPermitted(m, ir.Namespace) {
svhost := b.lookupSecureVirtualHost(host)
svhost.Secret = sec
svhost.MinProtoVersion = minProtoVersion(ir.Spec.VirtualHost.TLS.MinimumProtocolVersion)
enforceTLS = true
}
// passthrough is true if tls.secretName is not present, and
// tls.passthrough is set to true.
passthrough = tls.SecretName == "" && tls.Passthrough
}

switch {
case ir.Spec.TCPProxy != nil && (passthrough || enforceTLS):
b.processTCPProxy(ir, nil, host)
case ir.Spec.Routes != nil:
b.processRoutes(ir, "", nil, host, enforceTLS)
}
}
}

func (b *builder) secureVirtualhostExists(host string) bool {
_, ok := b.listener(b.externalSecurePort()).VirtualHosts[host]
return ok
}

// rulesFromSpec merges the IngressSpec's Rules with a synthetic
// rule representing the default backend.
func rulesFromSpec(spec v1beta1.IngressSpec) []v1beta1.IngressRule {
rules := spec.Rules
if backend := spec.Backend; backend != nil {
rule := defaultBackendRule(backend)
rules = append(rules, rule)
}
return rules
}

// defaultBackendRule returns an IngressRule that represents the IngressBackend.
func defaultBackendRule(be *v1beta1.IngressBackend) v1beta1.IngressRule {
return v1beta1.IngressRule{
IngressRuleValue: v1beta1.IngressRuleValue{
HTTP: &v1beta1.HTTPIngressRuleValue{
Paths: []v1beta1.HTTPIngressPath{{
Backend: v1beta1.IngressBackend{
ServiceName: be.ServiceName,
ServicePort: be.ServicePort,
},
}},
},
},
}
}

// DAG returns a *DAG representing the current state of this builder.
func (b *builder) DAG() *DAG {
var dag DAG
Expand Down

0 comments on commit fb273e6

Please sign in to comment.