-
Notifications
You must be signed in to change notification settings - Fork 680
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
Conversation
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 Also, I'm aware the design still isn't merged but thought this was enough to get us started and could easily adapt. |
e8332ca
to
f193ae1
Compare
f193ae1
to
3ffa92b
Compare
I've been doing some testing of this and I think it needs some more design thought:
|
internal/dag/builder.go
Outdated
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}), |
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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.
internal/dag/dag.go
Outdated
type UpstreamValidation struct { | ||
// Certificate holds a reference to the Secret containing the CA to be used to | ||
// verify the upstream connection. | ||
Certificate *Secret |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
4568cda
to
3fceb9f
Compare
3fceb9f
to
a8ed6fd
Compare
I think this is ready to go! =) |
There was a problem hiding this 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.
apis/contour/v1beta1/ingressroute.go
Outdated
// 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"` |
There was a problem hiding this comment.
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
docs/ingressroute.md
Outdated
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`. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
s/optional/manadatory
internal/dag/builder.go
Outdated
port: int32(port.IntValue()), | ||
strategy: strategy, | ||
healthcheck: healthcheckToString(hc), | ||
upstreamvalidation: upstreamValidationToString(uv), |
There was a problem hiding this comment.
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.
internal/dag/builder.go
Outdated
CACertificate: b.lookupSecret(meta{name: uv.CACertificate, namespace: svc.Namespace}), | ||
SubjectName: uv.SubjectName, | ||
} | ||
fmt.Println("-------- val found: ", val) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
debug code, please delete
internal/dag/builder.go
Outdated
}, | ||
Protocol: protocol, | ||
} | ||
b.services[s.toMeta()] = s | ||
return s | ||
} | ||
|
||
func (b *builder) addUpstreamValidation(uv *ingressroutev1.UpstreamValidation, svc *v1.Service) *UpstreamValidation { |
There was a problem hiding this comment.
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.
internal/e2e/cds_test.go
Outdated
@@ -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" |
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
internal/envoy/auth.go
Outdated
@@ -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 { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
s/cert/ca
internal/envoy/auth.go
Outdated
return context | ||
} | ||
|
||
func validationContext(cert []byte, subjaltname []string) *auth.CommonTlsContext_ValidationContext { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
two things
- 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
- Don't use nil to indicate a failure
- Reduce the propagation of nils through the data structures.
internal/envoy/cluster.go
Outdated
@@ -51,6 +60,22 @@ func Cluster(c *dag.Cluster) *v2.Cluster { | |||
} | |||
} | |||
|
|||
func upstreamValidationCACert(upstream *dag.HTTPService) []byte { | |||
if upstream.UpstreamValidation != nil { |
There was a problem hiding this comment.
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]
internal/envoy/cluster.go
Outdated
@@ -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 { |
There was a problem hiding this comment.
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. |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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
@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. |
9a34ba2
to
2dccfbf
Compare
2dccfbf
to
bb286ed
Compare
@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. |
bb286ed
to
5895bc5
Compare
subject alt name Signed-off-by: Steve Sloka <slokas@vmware.com>
5895bc5
to
0a2d06e
Compare
There was a problem hiding this 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}), |
There was a problem hiding this comment.
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 { |
There was a problem hiding this comment.
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 { |
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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 { |
There was a problem hiding this comment.
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 { |
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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, |
There was a problem hiding this comment.
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 { |
There was a problem hiding this comment.
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 { |
There was a problem hiding this comment.
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) |
There was a problem hiding this comment.
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
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