Skip to content

Commit fb273e6

Browse files
authored
Merge pull request projectcontour#915 from davecheney/tls-certificate-delegation
internal/dag: implement TLS Certificate Delegation
2 parents b50f5e8 + 6018cbf commit fb273e6

File tree

5 files changed

+463
-116
lines changed

5 files changed

+463
-116
lines changed

design/tls-certificate-delegation.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@ The implementation of this design is in three parts; the addition of a TLSCertif
3333

3434
### TLSCertificateDelegation CRD
3535

36-
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.
36+
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.
3737
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.
3838

3939
```

docs/ingressroute.md

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -240,6 +240,9 @@ spec:
240240
port: 80
241241
```
242242

243+
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`.
244+
See TLS Certificate Delegation below for more information.
245+
243246
The TLS **Minimum Protocol Version** a vhost should negotiate can be specified by setting the `spec.virtualhost.tls.minimumProtocolVersion`:
244247
- 1.3
245248
- 1.2
@@ -270,6 +273,42 @@ spec:
270273
permitInsecure: true
271274
```
272275

276+
#### TLS Certificate Delegation
277+
278+
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.
279+
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.
280+
281+
```yaml
282+
apiVersion: contour.heptio.com/v1beta1
283+
kind: TLSCertificateDelegation
284+
metadata:
285+
name: example-com-wildcard
286+
namespace: www-admin
287+
spec:
288+
delegations:
289+
secretName: example-com-wildcard
290+
targetNamespaces:
291+
- example-com
292+
---
293+
apiVersion: contour.heptio.com/v1beta1
294+
kind: IngressRoute
295+
metadata:
296+
name: www
297+
namespace: example-com
298+
spec:
299+
virtualhost:
300+
fqdn: foo2.bar.com
301+
tls:
302+
secretName: www-admin/example-com-wildcard
303+
routes:
304+
- match: /
305+
services:
306+
- name: s1
307+
port: 80
308+
```
309+
310+
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.
311+
273312
### Routing
274313

275314
Each route entry in an IngressRoute must start with a prefix match.

internal/dag/builder.go

Lines changed: 190 additions & 115 deletions
Original file line numberDiff line numberDiff line change
@@ -384,121 +384,12 @@ func (b *builder) compute() *DAG {
384384

385385
// setup secure vhosts if there is a matching secret
386386
// we do this first so that the set of active secure vhosts is stable
387-
// during the second ingress pass
388-
for _, ing := range b.source.ingresses {
389-
for _, tls := range ing.Spec.TLS {
390-
m := meta{name: tls.SecretName, namespace: ing.Namespace}
391-
if sec := b.lookupSecret(m); sec != nil {
392-
for _, host := range tls.Hosts {
393-
svhost := b.lookupSecureVirtualHost(host)
394-
svhost.Secret = sec
395-
svhost.MinProtoVersion = minProtoVersion(ing)
396-
}
397-
}
398-
}
399-
}
400-
401-
// deconstruct each ingress into routes and virtualhost entries
402-
for _, ing := range b.source.ingresses {
403-
// rewrite the default ingress to a stock ingress rule.
404-
rules := ing.Spec.Rules
405-
if backend := ing.Spec.Backend; backend != nil {
406-
rule := v1beta1.IngressRule{
407-
IngressRuleValue: v1beta1.IngressRuleValue{
408-
HTTP: &v1beta1.HTTPIngressRuleValue{
409-
Paths: []v1beta1.HTTPIngressPath{{
410-
Backend: v1beta1.IngressBackend{
411-
ServiceName: backend.ServiceName,
412-
ServicePort: backend.ServicePort,
413-
},
414-
}},
415-
},
416-
},
417-
}
418-
rules = append(rules, rule)
419-
}
420-
421-
for _, rule := range rules {
422-
host := rule.Host
423-
if host == "" {
424-
host = "*"
425-
}
426-
for _, httppath := range httppaths(rule) {
427-
prefix := httppath.Path
428-
if prefix == "" {
429-
prefix = "/"
430-
}
431-
432-
r := prefixRoute(ing, prefix)
433-
m := meta{name: httppath.Backend.ServiceName, namespace: ing.Namespace}
434-
if s := b.lookupHTTPService(m, httppath.Backend.ServicePort, 0, "", nil); s != nil {
435-
436-
r.addHTTPService(s)
437-
}
438-
439-
// should we create port 80 routes for this ingress
440-
if httpAllowed(ing) {
441-
b.lookupVirtualHost(host).addRoute(r)
442-
}
443-
if _, ok := b.listener(b.externalSecurePort()).VirtualHosts[host]; ok && host != "*" {
444-
b.lookupSecureVirtualHost(host).addRoute(r)
445-
}
446-
}
447-
}
448-
}
387+
// during computeIngresses.
388+
b.computeSecureVirtualhosts()
449389

450-
// process ingressroute documents
451-
for _, ir := range b.validIngressRoutes() {
452-
if ir.Spec.VirtualHost == nil {
453-
// mark delegate ingressroute orphaned.
454-
b.setOrphaned(ir)
455-
continue
456-
}
390+
b.computeIngresses()
457391

458-
// ensure root ingressroute lives in allowed namespace
459-
if !b.rootAllowed(ir) {
460-
b.setStatus(Status{Object: ir, Status: StatusInvalid, Description: "root IngressRoute cannot be defined in this namespace"})
461-
continue
462-
}
463-
464-
host := ir.Spec.VirtualHost.Fqdn
465-
if isBlank(host) {
466-
b.setStatus(Status{Object: ir, Status: StatusInvalid, Description: "Spec.VirtualHost.Fqdn must be specified"})
467-
continue
468-
}
469-
470-
var enforceTLS, passthrough bool
471-
if tls := ir.Spec.VirtualHost.TLS; tls != nil {
472-
// attach secrets to TLS enabled vhosts
473-
m := meta{name: tls.SecretName, namespace: ir.Namespace}
474-
if sec := b.lookupSecret(m); sec != nil {
475-
svhost := b.lookupSecureVirtualHost(host)
476-
svhost.Secret = sec
477-
enforceTLS = true
478-
479-
// process min protocol version
480-
switch ir.Spec.VirtualHost.TLS.MinimumProtocolVersion {
481-
case "1.3":
482-
svhost.MinProtoVersion = auth.TlsParameters_TLSv1_3
483-
case "1.2":
484-
svhost.MinProtoVersion = auth.TlsParameters_TLSv1_2
485-
default:
486-
// any other value is interpreted as TLS/1.1
487-
svhost.MinProtoVersion = auth.TlsParameters_TLSv1_1
488-
}
489-
}
490-
// passthrough is true if tls.secretName is not present, and
491-
// tls.passthrough is set to true.
492-
passthrough = tls.SecretName == "" && tls.Passthrough
493-
}
494-
495-
switch {
496-
case ir.Spec.TCPProxy != nil && (passthrough || enforceTLS):
497-
b.processTCPProxy(ir, nil, host)
498-
case ir.Spec.Routes != nil:
499-
b.processRoutes(ir, "", nil, host, enforceTLS)
500-
}
501-
}
392+
b.computeIngressRoutes()
502393

503394
return b.DAG()
504395
}
@@ -532,8 +423,8 @@ func isBlank(s string) bool {
532423

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

473+
// computeSecureVirtualhosts populates tls parameters of
474+
// secure virtual hosts.
475+
func (b *builder) computeSecureVirtualhosts() {
476+
for _, ing := range b.source.ingresses {
477+
for _, tls := range ing.Spec.TLS {
478+
m := splitSecret(tls.SecretName, ing.Namespace)
479+
if sec := b.lookupSecret(m); sec != nil && b.delegationPermitted(m, ing.Namespace) {
480+
for _, host := range tls.Hosts {
481+
svhost := b.lookupSecureVirtualHost(host)
482+
svhost.Secret = sec
483+
version := ing.Annotations["contour.heptio.com/tls-minimum-protocol-version"]
484+
svhost.MinProtoVersion = minProtoVersion(version)
485+
}
486+
}
487+
}
488+
}
489+
}
490+
491+
// splitSecret splits a secretName into its namespace and name components.
492+
// If there is no namespace prefix, the default namespace is returned.
493+
func splitSecret(secret, defns string) meta {
494+
v := strings.SplitN(secret, "/", 2)
495+
switch len(v) {
496+
case 1:
497+
// no prefix
498+
return meta{
499+
name: v[0],
500+
namespace: defns,
501+
}
502+
default:
503+
return meta{
504+
name: v[1],
505+
namespace: stringOrDefault(v[0], defns),
506+
}
507+
}
508+
}
509+
510+
func (b *builder) delegationPermitted(secret meta, to string) bool {
511+
contains := func(haystack []string, needle string) bool {
512+
if len(haystack) == 1 && haystack[0] == "*" {
513+
return true
514+
}
515+
for _, h := range haystack {
516+
if h == needle {
517+
return true
518+
}
519+
}
520+
return false
521+
}
522+
523+
if secret.namespace == to {
524+
// secret is in the same namespace as target
525+
return true
526+
}
527+
for _, d := range b.source.delegations {
528+
if d.Namespace != secret.namespace {
529+
continue
530+
}
531+
for _, d := range d.Spec.Delegations {
532+
if contains(d.TargetNamespaces, to) {
533+
if secret.name == d.SecretName {
534+
return true
535+
}
536+
}
537+
}
538+
}
539+
return false
540+
}
541+
542+
func (b *builder) computeIngresses() {
543+
// deconstruct each ingress into routes and virtualhost entries
544+
for _, ing := range b.source.ingresses {
545+
546+
// rewrite the default ingress to a stock ingress rule.
547+
rules := rulesFromSpec(ing.Spec)
548+
549+
for _, rule := range rules {
550+
host := stringOrDefault(rule.Host, "*")
551+
for _, httppath := range httppaths(rule) {
552+
prefix := stringOrDefault(httppath.Path, "/")
553+
r := prefixRoute(ing, prefix)
554+
be := httppath.Backend
555+
m := meta{name: be.ServiceName, namespace: ing.Namespace}
556+
if s := b.lookupHTTPService(m, be.ServicePort, 0, "", nil); s != nil {
557+
558+
r.addHTTPService(s)
559+
}
560+
561+
// should we create port 80 routes for this ingress
562+
if httpAllowed(ing) {
563+
b.lookupVirtualHost(host).addRoute(r)
564+
}
565+
566+
if b.secureVirtualhostExists(host) && host != "*" {
567+
b.lookupSecureVirtualHost(host).addRoute(r)
568+
}
569+
}
570+
}
571+
}
572+
}
573+
574+
func stringOrDefault(s, def string) string {
575+
if s == "" {
576+
return def
577+
}
578+
return s
579+
}
580+
581+
func (b *builder) computeIngressRoutes() {
582+
for _, ir := range b.validIngressRoutes() {
583+
if ir.Spec.VirtualHost == nil {
584+
// mark delegate ingressroute orphaned.
585+
b.setOrphaned(ir)
586+
continue
587+
}
588+
589+
// ensure root ingressroute lives in allowed namespace
590+
if !b.rootAllowed(ir) {
591+
b.setStatus(Status{Object: ir, Status: StatusInvalid, Description: "root IngressRoute cannot be defined in this namespace"})
592+
continue
593+
}
594+
595+
host := ir.Spec.VirtualHost.Fqdn
596+
if isBlank(host) {
597+
b.setStatus(Status{Object: ir, Status: StatusInvalid, Description: "Spec.VirtualHost.Fqdn must be specified"})
598+
continue
599+
}
600+
601+
var enforceTLS, passthrough bool
602+
if tls := ir.Spec.VirtualHost.TLS; tls != nil {
603+
// attach secrets to TLS enabled vhosts
604+
m := splitSecret(tls.SecretName, ir.Namespace)
605+
if sec := b.lookupSecret(m); sec != nil && b.delegationPermitted(m, ir.Namespace) {
606+
svhost := b.lookupSecureVirtualHost(host)
607+
svhost.Secret = sec
608+
svhost.MinProtoVersion = minProtoVersion(ir.Spec.VirtualHost.TLS.MinimumProtocolVersion)
609+
enforceTLS = true
610+
}
611+
// passthrough is true if tls.secretName is not present, and
612+
// tls.passthrough is set to true.
613+
passthrough = tls.SecretName == "" && tls.Passthrough
614+
}
615+
616+
switch {
617+
case ir.Spec.TCPProxy != nil && (passthrough || enforceTLS):
618+
b.processTCPProxy(ir, nil, host)
619+
case ir.Spec.Routes != nil:
620+
b.processRoutes(ir, "", nil, host, enforceTLS)
621+
}
622+
}
623+
}
624+
625+
func (b *builder) secureVirtualhostExists(host string) bool {
626+
_, ok := b.listener(b.externalSecurePort()).VirtualHosts[host]
627+
return ok
628+
}
629+
630+
// rulesFromSpec merges the IngressSpec's Rules with a synthetic
631+
// rule representing the default backend.
632+
func rulesFromSpec(spec v1beta1.IngressSpec) []v1beta1.IngressRule {
633+
rules := spec.Rules
634+
if backend := spec.Backend; backend != nil {
635+
rule := defaultBackendRule(backend)
636+
rules = append(rules, rule)
637+
}
638+
return rules
639+
}
640+
641+
// defaultBackendRule returns an IngressRule that represents the IngressBackend.
642+
func defaultBackendRule(be *v1beta1.IngressBackend) v1beta1.IngressRule {
643+
return v1beta1.IngressRule{
644+
IngressRuleValue: v1beta1.IngressRuleValue{
645+
HTTP: &v1beta1.HTTPIngressRuleValue{
646+
Paths: []v1beta1.HTTPIngressPath{{
647+
Backend: v1beta1.IngressBackend{
648+
ServiceName: be.ServiceName,
649+
ServicePort: be.ServicePort,
650+
},
651+
}},
652+
},
653+
},
654+
}
655+
}
656+
582657
// DAG returns a *DAG representing the current state of this builder.
583658
func (b *builder) DAG() *DAG {
584659
var dag DAG

0 commit comments

Comments
 (0)