Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add support for "projectcontour.io/tls-cert-namespace" annotation #4271

Merged
merged 7 commits into from
Jan 19, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions changelogs/unreleased/4271-pablo-ruth-small.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Adds a new Ingress annotation, `projectcontour.io/tls-cert-namespace`, to allow [TLS Certificate Delegation](https://projectcontour.io/docs/main/config/tls-delegation/) to be used with Ingress v1.
7 changes: 7 additions & 0 deletions internal/annotation/annotations.go
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@ var annotationsByKind = map[string]map[string]struct{}{
"projectcontour.io/response-timeout": {},
"projectcontour.io/retry-on": {},
"projectcontour.io/tls-minimum-protocol-version": {},
"projectcontour.io/tls-cert-namespace": {},
"projectcontour.io/websocket-routes": {},
},
"Service": {
Expand Down Expand Up @@ -147,6 +148,12 @@ func TLSRequired(i *networking_v1.Ingress) bool {
return i.Annotations["ingress.kubernetes.io/force-ssl-redirect"] == "true"
}

// TLSCertNamespace returns the namespace name of the delegated certificate if
// projectcontour.io/tls-cert-namespace annotation is present and non-empty
func TLSCertNamespace(i *networking_v1.Ingress) string {
return ContourAnnotation(i, "tls-cert-namespace")
}

// WebsocketRoutes retrieves the details of routes that should have websockets enabled from the
// associated websocket-routes annotation.
func WebsocketRoutes(i *networking_v1.Ingress) map[string]bool {
Expand Down
43 changes: 43 additions & 0 deletions internal/annotation/annotations_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -293,6 +293,49 @@ func TestWebsocketRoutes(t *testing.T) {
}
}

func TestTLSCertNamespace(t *testing.T) {
tests := map[string]struct {
a *networking_v1.Ingress
want string
}{
"absent": {
a: &networking_v1.Ingress{
ObjectMeta: metav1.ObjectMeta{
Annotations: map[string]string{},
},
},
want: "",
},
"empty": {
a: &networking_v1.Ingress{
ObjectMeta: metav1.ObjectMeta{
Annotations: map[string]string{
"projectcontour.io/tls-cert-namespace": "",
},
},
},
want: "",
},
"valid value": {
a: &networking_v1.Ingress{
ObjectMeta: metav1.ObjectMeta{
Annotations: map[string]string{
"projectcontour.io/tls-cert-namespace": "namespace-with-cert",
},
},
},
want: "namespace-with-cert",
},
}

for name, tc := range tests {
t.Run(name, func(t *testing.T) {
got := TLSCertNamespace(tc.a)
assert.Equal(t, tc.want, got)
})
}
}

func TestHttpAllowed(t *testing.T) {
tests := map[string]struct {
i *networking_v1.Ingress
Expand Down
72 changes: 72 additions & 0 deletions internal/dag/builder_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -4745,6 +4745,27 @@ func TestDAGInsert(t *testing.T) {
},
}

// i18V1 is use secret from another namespace using annotation projectcontour.io/tls-cert-namespace
i18V1 := &networking_v1.Ingress{
ObjectMeta: metav1.ObjectMeta{
Name: "tls-from-other-ns-annotation",
Namespace: "default",
Annotations: map[string]string{
"projectcontour.io/tls-cert-namespace": sec4.Namespace,
},
},
Spec: networking_v1.IngressSpec{
TLS: []networking_v1.IngressTLS{{
Hosts: []string{"kuard.example.com"},
SecretName: sec4.Name,
}},
Rules: []networking_v1.IngressRule{{
Host: "kuard.example.com",
IngressRuleValue: ingressrulev1value(backendv1("kuard", intstr.FromInt(8080))),
}},
},
}

iPathMatchTypesV1 := &networking_v1.Ingress{
ObjectMeta: metav1.ObjectMeta{
Name: "pathmatchtypes",
Expand Down Expand Up @@ -10237,6 +10258,57 @@ func TestDAGInsert(t *testing.T) {
},
),
},
"ingressv1: insert service, secret, then ingress w/ tls and delegation annotation (missing delegation)": {
objs: []interface{}{
s1,
sec4,
i18V1,
},
want: listeners(
&Listener{
Name: HTTP_LISTENER_NAME,
Port: 80,
VirtualHosts: virtualhosts(
virtualhost("kuard.example.com", prefixroute("/", service(s1))),
),
},
),
},
"ingressv1: insert service, secret, delegation, then ingress w/ tls and delegation annotation": {
objs: []interface{}{
s1,
sec4,
&contour_api_v1.TLSCertificateDelegation{
ObjectMeta: metav1.ObjectMeta{
Name: "CertDelagation",
Namespace: sec4.Namespace,
},
Spec: contour_api_v1.TLSCertificateDelegationSpec{
Delegations: []contour_api_v1.CertificateDelegation{{
SecretName: sec4.Name,
TargetNamespaces: []string{"*"},
}},
},
},
i18V1,
},
want: listeners(
&Listener{
Name: HTTP_LISTENER_NAME,
Port: 80,
VirtualHosts: virtualhosts(
virtualhost("kuard.example.com", prefixroute("/", service(s1))),
),
},
&Listener{
Name: HTTPS_LISTENER_NAME,
Port: 443,
SecureVirtualHosts: securevirtualhosts(
securevirtualhost("kuard.example.com", sec4, prefixroute("/", service(s1))),
),
},
),
},
"insert ingress with externalName service": {
objs: []interface{}{
ingressExternalNameService,
Expand Down
2 changes: 1 addition & 1 deletion internal/dag/ingress_processor.go
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,7 @@ func (p *IngressProcessor) Run(dag *DAG, source *KubernetesCache) {
func (p *IngressProcessor) computeSecureVirtualhosts() {
for _, ing := range p.source.ingresses {
for _, tls := range ing.Spec.TLS {
secretName := k8s.NamespacedNameFrom(tls.SecretName, k8s.DefaultNamespace(ing.GetNamespace()))
secretName := k8s.NamespacedNameFrom(tls.SecretName, k8s.TLSCertAnnotationNamespace(ing), k8s.DefaultNamespace(ing.GetNamespace()))
sec, err := p.source.LookupSecret(secretName, validSecret)
if err != nil {
p.WithError(err).
Expand Down
54 changes: 54 additions & 0 deletions internal/featuretests/v3/tlscertificatedelegation_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ import (
"github.com/projectcontour/contour/internal/featuretests"
"github.com/projectcontour/contour/internal/fixture"
v1 "k8s.io/api/core/v1"
networking_v1 "k8s.io/api/networking/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
)

Expand Down Expand Up @@ -261,4 +262,57 @@ func TestTLSCertificateDelegation(t *testing.T) {
),
TypeUrl: listenerType,
})

rh.OnDelete(hp1)

c.Request(listenerType).Equals(&envoy_discovery_v3.DiscoveryResponse{
Resources: resources(t,
statsListener(),
),
TypeUrl: listenerType,
})

// add an ingress in a different namespace mentioning secret wildcard from namespace secret via annotation.
i1 := &networking_v1.Ingress{
ObjectMeta: metav1.ObjectMeta{
Name: "simple",
Namespace: s1.Namespace,
Annotations: map[string]string{
"projectcontour.io/tls-cert-namespace": sec1.Namespace,
},
},
Spec: networking_v1.IngressSpec{
TLS: []networking_v1.IngressTLS{{
Hosts: []string{"example.com"},
SecretName: sec1.Name,
}},
Rules: []networking_v1.IngressRule{{
Host: "example.com",
IngressRuleValue: networking_v1.IngressRuleValue{
HTTP: &networking_v1.HTTPIngressRuleValue{
Paths: []networking_v1.HTTPIngressPath{{
Backend: networking_v1.IngressBackend{
Service: &networking_v1.IngressServiceBackend{
Name: s1.Name,
Port: networking_v1.ServiceBackendPort{
Number: 8080,
},
},
},
}},
},
},
}},
},
}
rh.OnAdd(i1)

c.Request(listenerType).Equals(&envoy_discovery_v3.DiscoveryResponse{
Resources: resources(t,
defaultHTTPListener(),
ingressHTTPS,
statsListener(),
),
TypeUrl: listenerType,
})
}
12 changes: 12 additions & 0 deletions internal/k8s/objectmeta.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@ package k8s
import (
"strings"

"github.com/projectcontour/contour/internal/annotation"
networking_v1 "k8s.io/api/networking/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/types"
)
Expand All @@ -34,6 +36,16 @@ func NamespacedNameOf(obj metav1.Object) types.NamespacedName {
return name
}

// TLSCertAnnotationNamespace can be used with NamespacedNameFrom to set the secret namespace
// from the "projectcontour.io/tls-cert-namespace" annotation
func TLSCertAnnotationNamespace(ing *networking_v1.Ingress) func(name *types.NamespacedName) {
return func(name *types.NamespacedName) {
if name.Namespace == "" {
name.Namespace = annotation.TLSCertNamespace(ing)
}
}
}

// DefaultNamespace can be used with NamespacedNameFrom to set the
// default namespace for a resource name that may not be qualified by
// a namespace.
Expand Down
3 changes: 3 additions & 0 deletions site/content/docs/main/config/annotations.md
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@ The `ingress.kubernetes.io/force-ssl-redirect` annotation takes precedence over
- `projectcontour.io/retry-on`: [The conditions for Envoy to retry a request][5]. See also [possible values and their meanings for `retry-on`][6].
- `projectcontour.io/tls-minimum-protocol-version`: [The minimum TLS protocol version][7] the TLS listener should support. Valid options are `1.3`, `1.2` (default), `1.1`.
- `projectcontour.io/websocket-routes`: [The routes supporting websocket protocol][8], the annotation value contains a list of route paths separated by a comma that must match with the ones defined in the `Ingress` definition. Defaults to Envoy's default behavior which is `use_websocket` to `false`.
- `projectcontour.io/tls-cert-namespace`: The namespace where all TLS secrets of this Ingress are searched. This is necessary to use [TLS Certificate Delegation][18] with Ingress v1 because the slash notation (ex: different-ns/app-cert) used by HTTPProxy and Ingress v1beta1 is not accepted. See [this issue][19] for details.

## Contour specific Service annotations

Expand Down Expand Up @@ -88,3 +89,5 @@ A [Kubernetes Service][9] maps to an [Envoy Cluster][10]. Envoy clusters have ma
[15]: fundamentals.md
[16]: https://www.envoyproxy.io/docs/envoy/latest/api-v3/config/route/v3/route_components.proto#envoy-v3-api-field-config-route-v3-virtualhost-require-tls
[17]: api/#projectcontour.io/v1.UpstreamValidation
[18]: ../config/tls-delegation/
[19]: https://github.com/projectcontour/contour/issues/3544
41 changes: 35 additions & 6 deletions site/content/docs/main/config/tls-delegation.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,7 @@

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 referencing the TLS certificate, permission to Contour to read the Secret object from another namespace.
Delegation works for both HTTPProxy and Ingress v1beta1 resources (however it does not work with Ingress v1).
TLS Certificate Delegation is not currently supported on Ingress v1 resources due to changes in the spec that make this impossible.
See [this issue][0] for details.
Delegation works for both HTTPProxy and Ingress resources, however it needs an annotation to work with Ingress v1.

The [`TLSCertificateDelegation`][1] resource defines a set of `delegations` in the `spec`.
Each delegation references a `secretName` from the namespace where the `TLSCertificateDelegation` is created as well as describing a set of `targetNamespaces` in which the certificate can be referenced.
Expand All @@ -24,7 +22,13 @@ spec:
- secretName: another-com-wildcard
targetNamespaces:
- "*"
---
```

In this example, the permission for Contour to reference the Secret `example-com-wildcard` in the `admin` namespace has been delegated to HTTPProxy and Ingress objects in the `example-com` namespace.
Also, the permission for Contour to reference the Secret `another-com-wildcard` from all namespaces has been delegated to all HTTPProxy and Ingress objects in the cluster.

To reference the secret from an HTTPProxy or Ingress v1beta1 you must use the slash syntax in the `secretName`:
```yaml
apiVersion: projectcontour.io/v1
kind: HTTPProxy
metadata:
Expand All @@ -41,8 +45,33 @@ spec:
port: 80
```

In this example, the permission for Contour to reference the Secret `example-com-wildcard` in the `admin` namespace has been delegated to HTTPProxy objects in the `example-com` namespace.
Also, the permission for Contour to reference the Secret `another-com-wildcard` from all namespaces has been delegated to all HTTPProxy objects in the cluster.
To reference the secret from an Ingress v1 you must use the `projectcontour.io/tls-cert-namespace` annotation:
```yaml
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
annotations:
projectcontour.io/tls-cert-namespace: www-admin
name: www
namespace: example-com
spec:
rules:
- host: foo2.bar.com
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: s1
port:
number: 80
tls:
- hosts:
- foo2.bar.com
secretName: example-com-wildcard
```


[0]: https://github.com/projectcontour/contour/issues/3544
[1]: /docs/{{< param version >}}/config/api/#projectcontour.io/v1.TLSCertificateDelegation