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

Implement upstream tls backend verification with ca cert and required san #1045

Merged
merged 1 commit into from
May 9, 2019

Conversation

stevesloka
Copy link
Member

Fixes #813 by implementing tls backend verification via CA cert as well as an optional subject alt name verification on the cert.

Signed-off-by: Steve Sloka slokas@vmware.com

@stevesloka
Copy link
Member Author

I still need to do a full test of this on a live cluster, but wanted to get the code out in front of eyes. One thing I want to do is const the ca.crt in the key unless we go with a dynamic route.

Also, I'm aware the design still isn't merged but thought this was enough to get us started and could easily adapt.

@stevesloka
Copy link
Member Author

I've been doing some testing of this and I think it needs some more design thought:

  1. If I specify the tls annotation on the service, I can immediately proxy tls to the upstream without verification. This may not be a desire that administrators want to allow, nothing forces a user to specify the ca cert. I wonder if we should rename the validation struct we added to the route's service and add what functionality the tls annotation is doing. (@davecheney)
  2. If the cert is missing then the same for Make bootstrap mode optional #1 above applies. I think we should also be setting the ingressroute status and reject the change since it's invalid.
  3. I need to further test, but I'm not sure if rotating a cert happened automatically which it should

func (b *builder) addUpstreamValidation(uv *ingressroutev1.UpstreamValidation, svc *v1.Service) *UpstreamValidation {
if uv != nil {
val := &UpstreamValidation{
Certificate: b.lookupSecret(meta{name: uv.CASecret, namespace: svc.Namespace}),
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The current strawman design specifies that the secret should be in the same namespace as the ingress route instead of the service: https://github.com/heptio/contour/pull/1040/files#diff-aa37a99afb27f0131b45f334343232beR70

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yes that is correct. You can't reference a service from a different namespace from an IngressRoute and I need a way to see what namespace the IngressRoute is currently located in.

type UpstreamValidation struct {
// Certificate holds a reference to the Secret containing the CA to be used to
// verify the upstream connection.
Certificate *Secret
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think it would be good for the for the field name to be clear that it's a CA certificate.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah I agree that more clear.

@stevesloka stevesloka changed the title Implement upstream tls backend verification with ca cert and optional san wip: Implement upstream tls backend verification with ca cert and optional san Apr 28, 2019
@stevesloka stevesloka changed the title wip: Implement upstream tls backend verification with ca cert and optional san Implement upstream tls backend verification with ca cert and optional san Apr 29, 2019
@stevesloka stevesloka added this to the 0.12.0 milestone Apr 29, 2019
@stevesloka
Copy link
Member Author

I think this is ready to go! =)

Copy link
Contributor

@davecheney davecheney left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thank you for working on this. There are a number of minor nits

The major concern I have is CA validation is a property of the path from route to service, it is neither a property of the service, or the route; many routes can point to the same service, and many services can be pointed to by one route.

The dag.Cluster object is intended to hold these properties -- there are others that are currently on service which should not be, we'll have to take those as they come.

// Name of the Kubernetes secret be used to validate the certificate presented by the backend
CACertificate string `json:"caSecret"`
// Ket which is expected to be present in the 'subjectAltName' of the presented certificate
SubjectName string `json:"subjectName,omitempty"`
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please remove omit empty, I decided at the last minute that validation of the CA is not sufficient. We'll make both of these parameters mandatory in 0.12

This annoation tells Contour which port should be used for the TLS connection.
In this example, the upstream service is named `https` and uses port `443`.
Additionally, it is possible for Envoy to verify the backend service's certificate.
The service of an `IngressRoute` can optionally specify a `validation` struct which has a manditory `caSecret` key as well as an optional `subjectName`.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

s/optional/manadatory

port: int32(port.IntValue()),
strategy: strategy,
healthcheck: healthcheckToString(hc),
upstreamvalidation: upstreamValidationToString(uv),
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sorry, this is not correct. The upstream validation is not a property of the service, it is a propery of the cluster -- the path from the route to the service.

CACertificate: b.lookupSecret(meta{name: uv.CACertificate, namespace: svc.Namespace}),
SubjectName: uv.SubjectName,
}
fmt.Println("-------- val found: ", val)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

debug code, please delete

},
Protocol: protocol,
}
b.services[s.toMeta()] = s
return s
}

func (b *builder) addUpstreamValidation(uv *ingressroutev1.UpstreamValidation, svc *v1.Service) *UpstreamValidation {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

please don't call this add, it doesn't add anything to the state of the builder. lookupUpstreamValidation might be a better name, but perhaps a different design is needed.

@@ -18,14 +18,14 @@ import (
"testing"
"time"

"github.com/envoyproxy/go-control-plane/envoy/api/v2"
v2 "github.com/envoyproxy/go-control-plane/envoy/api/v2"
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

goimports turds here and elsewhere

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is there a way to disable this? I can turn off the imports, but it's nice to manage for me.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not sure, it just started to happen to me when I upgraded to vim-go 1.20. Maybe we need to file an issue upstream about goimports being silly.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is apparently by design: golang/go#30051 (comment)

I'll turn off goimports for now and fix up manually.

@@ -47,12 +47,34 @@ var (
// UpstreamTLSContext creates an auth.UpstreamTlsContext. By default
// UpstreamTLSContext returns a HTTP/1.1 TLS enabled context. A list of
// additional ALPN protocols can be provided.
func UpstreamTLSContext(alpnProtocols ...string) *auth.UpstreamTlsContext {
return &auth.UpstreamTlsContext{
func UpstreamTLSContext(cert []byte, subjaltname []string, alpnProtocols ...string) *auth.UpstreamTlsContext {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

s/cert/ca

return context
}

func validationContext(cert []byte, subjaltname []string) *auth.CommonTlsContext_ValidationContext {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

two things

  1. please put the guard clause first; ie
if cert == nil {
   // no validation required
   return nil

b. please don't push this logic into validationContext. If its not supposed to be called with cert == nil, then don't call it with cert == nil. The reason I ask this is no validationContext sometimes returns nil depending on its inputs, that means we have nils floating around the data structures, this is how you get java :). We should at all times be following these two rules

  1. Don't use nil to indicate a failure
  2. Reduce the propagation of nils through the data structures.

@@ -51,6 +60,22 @@ func Cluster(c *dag.Cluster) *v2.Cluster {
}
}

func upstreamValidationCACert(upstream *dag.HTTPService) []byte {
if upstream.UpstreamValidation != nil {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please change these conditions so that the success case proceeds down the page. The guard clause should be

if upstreamValidation == nil {
   // right, it's nil, what next
  return nil
}
return upstream.UpstreamValidation.CACertificate.Object.Data[CACertificateKey]

@@ -147,6 +172,12 @@ func Clustername(cluster *dag.Cluster) string {
}
buf += hc.Path
}
if uv := service.UpstreamValidation; uv != nil {
buf += uv.CACertificate.Object.ObjectMeta.Name
if len(uv.SubjectName) > 0 {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

you can elide this check

buf += "" is just buf

@@ -40,7 +40,7 @@ type CertificateDelegation struct {
// +genclient
// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object

// TLSCertificateDelefgation is an TLS Certificate Delegation CRD specificiation.
// TLSCertificateDelefgation is an TLS CACertificate Delegation CRD specificiation.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please revert this, I think it was an unintended change.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The comment also has a typo: TLSCertificateDelefgation

@davecheney
Copy link
Contributor

@stevesloka ping, this is over to you to respond to review comments. Please ping me when it's ready for the next round of review.

@stevesloka stevesloka force-pushed the tlsVerification branch 4 times, most recently from 9a34ba2 to 2dccfbf Compare May 8, 2019 20:15
@stevesloka stevesloka changed the title Implement upstream tls backend verification with ca cert and optional san Implement upstream tls backend verification with ca cert and required san May 8, 2019
@stevesloka
Copy link
Member Author

@davecheney I think this is ready for another look. The travis build keeps failing, not sure why, I'll kick it again in a bit.

subject alt name

Signed-off-by: Steve Sloka <slokas@vmware.com>
@davecheney davecheney merged commit 0a2d06e into projectcontour:master May 9, 2019
Copy link
Contributor

@davecheney davecheney left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thank you for working on this. I've addressed the comments inline while committing this change. Thank you.

return nil
}
return &UpstreamValidation{
CACertificate: b.lookupSecret(meta{name: uv.CACertificate, namespace: namespace}),
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If the secret is not found then we should return nil here,

also if the subjectname is blank we should return nil here.

@@ -668,7 +668,20 @@ func (b *builder) processRoutes(ir *ingressroutev1.IngressRoute, prefixMatch str
}
m := meta{name: service.Name, namespace: ir.Namespace}
if s := b.lookupHTTPService(m, intstr.FromInt(service.Port), service.Strategy, service.HealthCheck); s != nil {
r.addHTTPService(s, service.Weight)
uv := b.lookupUpstreamValidation(service.UpstreamValidation, ir.Namespace)
if service.UpstreamValidation != nil {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If service.UpstreamValidation is nil then uv will be nil, so the check should probably be

if service.UpstreamValidation != nil && uv == nil {
// upstream validation requested, some components were missing

r.addHTTPService(s, service.Weight)
uv := b.lookupUpstreamValidation(service.UpstreamValidation, ir.Namespace)
if service.UpstreamValidation != nil {
if uv.CACertificate == nil {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

but you do need to do this check because uv can be nil if lookupUpstreamValidation returned nil because there was a problem

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

theproblem is probably that lookupUPstreamValidation cannot be factored out of this call, we probably need to construct the uv inline while checking each value and setting status.

@@ -714,6 +727,16 @@ func (b *builder) processRoutes(ir *ingressroutev1.IngressRoute, prefixMatch str
b.setStatus(Status{Object: ir, Status: StatusValid, Description: "valid IngressRoute", Vhost: host})
}

func (b *builder) lookupUpstreamValidation(uv *ingressroutev1.UpstreamValidation, namespace string) *UpstreamValidation {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This could do with some unit tests. Its annoying to test as written because of all the parameters but I have an idea to really DRY up the testing of lookup helpers without the horror show of buidler_test.go

I'll leave this as a TODO

@@ -46,12 +46,35 @@ var (
// UpstreamTLSContext creates an auth.UpstreamTlsContext. By default
// UpstreamTLSContext returns a HTTP/1.1 TLS enabled context. A list of
// additional ALPN protocols can be provided.
func UpstreamTLSContext(alpnProtocols ...string) *auth.UpstreamTlsContext {
return &auth.UpstreamTlsContext{
func UpstreamTLSContext(ca []byte, subjaltname []string, alpnProtocols ...string) *auth.UpstreamTlsContext {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

subjectaltname is a slice, the name should probably be a plural; subjectNames would be fine i think

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In fact we can probably make it easier on ourselves by only passing a single subject name -- that's all we allow from ingressroute so we can hide the fact that envoy takes a slice of strings, that's a triviality.

ValidationContext: &auth.CertificateValidationContext{
TrustedCa: &core.DataSource{
Specifier: &core.DataSource_InlineBytes{
InlineBytes: ca,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

we need a todo to update this for SDS

}

func validationContext(ca []byte, subjaltname []string) *auth.CommonTlsContext_ValidationContext {
if ca == nil {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This isn't quite correct; if validationContext returns nil if ca is nil, it should also return nil if subjectname contains < 1 element and that element is blank.

@@ -508,3 +573,11 @@ func service(s *v1.Service) dag.TCPService {
ServicePort: &s.Spec.Ports[0],
}
}

func tlsservice(s *v1.Service, cert, subjectaltname string) dag.TCPService {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

cert and subjectaltname are not used

CommonTlsContext: &auth.CommonTlsContext{
AlpnProtocols: alpnProtocols,
},
}

validation := validationContext(ca, subjaltname)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

urgh, it turns out we have to do it this way because passing back a nil here creates a typed nil because context.CommonTlsContext.ValidationContextType is an interface. This comment is too short to explain why, but I'll leave a comment in the code to explain why

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Support TLS verification of backend services
3 participants