Skip to content

Support for SSL BackendSet certificate #243

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

Merged
merged 5 commits into from
Sep 21, 2018
Merged

Support for SSL BackendSet certificate #243

merged 5 commits into from
Sep 21, 2018

Conversation

MadalinaPatrichi
Copy link
Member

@MadalinaPatrichi MadalinaPatrichi commented Sep 10, 2018

Added support for supplying SSL certificates for LoadBalancers backend sets:

  1. Added support for new type of generic Kubernetes secret to read from, which has the following data fields:
  • ca.crt - Base64 encoded CA certificate
  • tls.crt - Base64 encoded Public certificate
  • tls.key - Base64 encoded Private key
  • passphrase - Base64 encoded passphrase
  1. Modified Health Check to support both TCP and HTTP protocols. The choice between protocols is driven by whether there exists an SSL certificate associated to the port for which the health check is being done. Both the SSL certificates and the SSL enabled ports are specified in the annotations of the Load Balancer configuration file

  2. Added Ginkgo test to check connectivity for SSL enabled backend sets

secretString, ok := svc.Annotations[ServiceAnnotationLoadBalancerTLSSecret]
if !ok {
return "", "", errors.Errorf("no %q annotation found", ServiceAnnotationLoadBalancerTLSSecret)
func (cp *CloudProvider) readSSLSecret(secretType string, svc *v1.Service) (string, string, string, string, error) {
Copy link
Member

@owainlewis owainlewis Sep 11, 2018

Choose a reason for hiding this comment

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

The return type here is pretty hard to understand and might be prone to assignment error. Suggest refactoring into a struct or similar.

func (cp *CloudProvider) readSSLSecret(secretType string, svc *v1.Service) (*TLSCertificate, error)

Copy link
Member Author

Choose a reason for hiding this comment

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

You're right, thanks!

@owainlewis
Copy link
Member

Minor typo in commit message: Add Backendset SSL cerrtificates => Add Backendset SSL certificates

@MadalinaPatrichi MadalinaPatrichi force-pushed the mp/TLSImplementation branch 3 times, most recently from b2ffd66 to 079b329 Compare September 13, 2018 08:49
@MadalinaPatrichi MadalinaPatrichi changed the title [WIP] Support for SSL backend certificate Support for SSL backend certificate Sep 13, 2018
@MadalinaPatrichi MadalinaPatrichi changed the title Support for SSL backend certificate Support for SSL BackendSet certificate Sep 13, 2018
@MadalinaPatrichi MadalinaPatrichi force-pushed the mp/TLSImplementation branch 2 times, most recently from cdf2d44 to 30084bb Compare September 13, 2018 09:22
@owainlewis owainlewis removed the request for review from simonlord September 13, 2018 13:55
@owainlewis owainlewis self-assigned this Sep 13, 2018
if !exists {
return cp.createLoadBalancer(ctx, spec)
}

// for _, v := range spec.BackendSets {
Copy link
Member

Choose a reason for hiding this comment

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

Let's remove this

Copy link
Member Author

Choose a reason for hiding this comment

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

Done

@MadalinaPatrichi MadalinaPatrichi force-pushed the mp/TLSImplementation branch 2 times, most recently from 429affc to f14a7e8 Compare September 13, 2018 14:20
Copy link
Contributor

@prydie prydie left a comment

Choose a reason for hiding this comment

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

👍

targetPort: 80
```

First the required secret needs to be created in Kubernetes. For the purposes
Copy link
Contributor

Choose a reason for hiding this comment

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

s/secret/Secret/

production you would most likely use a public certificate signed by a
certificate authority.

Below is an example of a secret configuration file required to be uploaded as a Kubernetes
Copy link
Contributor

Choose a reason for hiding this comment

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

s/secret/Secret/

certificate authority.

Below is an example of a secret configuration file required to be uploaded as a Kubernetes
generic secret. The CA certificate, the public certificate and the private key need to be base64 encoded :
Copy link
Contributor

Choose a reason for hiding this comment

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

s/secret/Secret/

Copy link
Contributor

Choose a reason for hiding this comment

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

Space before :

tls.key: LS0tLS1CRUdJTi(...)
```

Self-signed certificates can be generated using the following GitHub repository: https://github.com/square/certstrap
Copy link
Contributor

Choose a reason for hiding this comment

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

Maybe cfssl (or god forbid even openssl) would be a more standard recommendation?

Copy link
Member Author

Choose a reason for hiding this comment

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

Haha, true, I'll just remove it, Google can help if needed

// ServiceAnnotationLoadBalancerTLSBackendSecret is a Service annotation for
// specifying the TLS secret to install on the load balancer listeners which
// have SSL enabled.
// See: https://kubernetes.io/docs/concepts/services-networking/ingress/#tls
Copy link
Contributor

Choose a reason for hiding this comment

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

This probably needs updating to reflect that we aren't using TLS Secrets.

@@ -230,7 +249,8 @@ func (cp *CloudProvider) ensureSSLCertificate(ctx context.Context, lb *loadbalan
}

// Although we iterate here only one certificate is supported at the moment.
certs, err := spec.Certificates()
certs := make(map[string]loadbalancer.CertificateDetails)
Copy link
Contributor

Choose a reason for hiding this comment

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

This is a strange pattern. Any reason you're not allocating the map in the function and returning?

Copy link
Member Author

@MadalinaPatrichi MadalinaPatrichi Sep 14, 2018

Choose a reason for hiding this comment

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

It's because I wanted to hold all certificates (both for listeners and backend sets) within the same map. Therefore, I am creating a map of certificates and I add to it step by step. I guess this saves me from having to make a union of the map :) for which go doesn't have a nice implementation of (unlike python)

Copy link
Contributor

Choose a reason for hiding this comment

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

Ah, that makes sense. I think perhaps then that extending SSLConfig to include both BackendSet and Listener SSL configuration would likely result in cleaner code. Thoughts?

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, you're right, Or maybe have a separate struct to hold both

// specifying the TLS secret to install on the load balancer listeners which
// have SSL enabled.
// See: https://kubernetes.io/docs/concepts/services-networking/ingress/#tls
ServiceAnnotationLoadBalancerTLSBackendSecret = "service.beta.kubernetes.io/oci-load-balancer-tls-backend-secret"
Copy link
Contributor

Choose a reason for hiding this comment

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

Do we also need to support something along the lines of ServiceAnnotationLoadBalancerSSLPorts but for backends at all?

Copy link
Member Author

Choose a reason for hiding this comment

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

I thought of this too, but @owainlewis was mentioning that we can assume that both the Listener certificate and the Backend set certificates can be uniformly applied for the same ports. The closest thing to what OCI offers would be to have a one-to-one mapping between one port and one certificate, which is not the case now, even with Listeners. Maybe we should have a discussion about this and address this separately


return string(cert), string(key), nil
if key, ok = secret.Data[SSLPrivateKeyFileName]; !ok {
return &SSLK8SSecret{}, errors.Errorf("%s not found in secret %s/%s", SSLPrivateKeyFileName, ns, name)
Copy link
Contributor

Choose a reason for hiding this comment

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

return nil, errors.Errorf("...

@@ -29,8 +29,14 @@ import (
)

const (
sslCertificateFileName = "tls.crt"
sslPrivateKeyFileName = "tls.key"
// SSLCAFileName - Key name for ca data in the secrets config
Copy link
Contributor

Choose a reason for hiding this comment

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

Docstring comments should read as a sentence:

	// SSLCAFileName is the key name for ca data in the secrets config.

@MadalinaPatrichi MadalinaPatrichi force-pushed the mp/TLSImplementation branch 5 times, most recently from 952a2c1 to b7efcb9 Compare September 17, 2018 13:45
@@ -349,6 +434,23 @@ func GetReadySchedulableNodesOrDie(c clientset.Interface) (nodes *v1.NodeList) {
return nodes
}

// NewSecretData returns the default v1.Secret template for this jig, but
// does not actually create the Secret.
func NewSecretData(ns, generateName string, caCert, cert, privateKey, passphrase []byte) *v1.Secret {
Copy link
Contributor

Choose a reason for hiding this comment

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

Appears unused.

path, port := apiservice.GetServiceHealthCheckPathPort(svc)
if path != "" {
func getHealthChecker(cfg *SSLConfig, port int, svc *v1.Service) *loadbalancer.HealthCheckerDetails {
var protocol string
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 health-check has SSL enabled use TCP rather than HTTP.
protocol := lbNodesHealthCheckProtoHTTP
if cfg != nil && cfg.Ports.Has(port) {
	protocol = lbNodesHealthCheckProtoTCP
}		

@@ -29,8 +29,14 @@ import (
)

const (
sslCertificateFileName = "tls.crt"
sslPrivateKeyFileName = "tls.key"
// SSLCAFileName is a key name for ca data in the secrets config
Copy link
Contributor

Choose a reason for hiding this comment

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

As docstrings are expected to be sentences (for nice godocs) they should end with a full-stop.

SSLCAFileName = "ca.crt"
// SSLCertificateFileName is a key name for certificate data in the secrets config
SSLCertificateFileName = "tls.crt"
// SSLPrivateKeyFileName - is a key name for cartificate private key in the secrets config
Copy link
Contributor

Choose a reason for hiding this comment

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

s/-// (sentences)


ipPort := net.JoinHostPort(ip, strconv.Itoa(port))
url := fmt.Sprintf("http://%s%s", ipPort, request)
Copy link
Contributor

Choose a reason for hiding this comment

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

url := fmt.Sprintf("http://%s%s", ipPort, request)
if secure {
	url = fmt.Sprintf("https://%s%s", ipPort, request)
}

Copy link
Contributor

@prydie prydie left a comment

Choose a reason for hiding this comment

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

Couple of nits and queries but otherwise looking awesome 👍

ns := f.Namespace.Name

jig := framework.NewServiceTestJig(f.ClientSet, serviceName)
//nodeIP := framework.PickNodeIP(jig.Client) // for later
Copy link
Contributor

Choose a reason for hiding this comment

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

Should probably clean up commented out code?


ipPort := net.JoinHostPort(ip, strconv.Itoa(port))
url := fmt.Sprintf("http://%s%s", ipPort, request)
var url string
Copy link
Contributor

Choose a reason for hiding this comment

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

url := fmt.Sprintf("http://%s%s", ipPort, request)

(no need for separate declare and define)

if !c.rateLimiter.Writer.TryAccept() {
return "", RateLimitError(true, "CreateCertificate")
}

// TODO(apryde): We currently don't have a mechanism for supplying
// CreateCertificateDetails.CaCertificate.
resp, err := c.loadbalancer.CreateCertificate(ctx, loadbalancer.CreateCertificateRequest{
LoadBalancerId: &lbID,
CreateCertificateDetails: loadbalancer.CreateCertificateDetails{
Copy link
Contributor

Choose a reason for hiding this comment

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

Could you not just do CreateCertificateDetails: cert, here?

Copy link
Member Author

Choose a reason for hiding this comment

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

No, one is CreateCertificateDetails type and one is CertificateDetails. Unless there's some magic to make that happen? :)

Copy link
Member Author

Choose a reason for hiding this comment

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

please say there is to make go cooler 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.

🤦‍♂️

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 that a facepalm because it should have been obvious for me on how to cast it from one type to the other?

Copy link
Contributor

Choose a reason for hiding this comment

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

Goodness no! Facepalm that I didn't notice they were different types 😄

return "", "", errors.Errorf("%s not found in secret %s/%s", sslCertificateFileName, ns, name)
var ok bool
var cacert, cert, key, pass []byte
cacert, _ = secret.Data[SSLCAFileName]
Copy link
Contributor

Choose a reason for hiding this comment

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

	cacert = secret.Data[SSLCAFileName]

if requiresCertificate(service) {
ports, err := getSSLEnabledPorts(service)
if err != nil {
return nil, err
}
ssl = NewSSLConfig(lbName, ports, cp)
secretListenerString, _ := service.Annotations[ServiceAnnotationLoadBalancerTLSSecret]
Copy link
Contributor

Choose a reason for hiding this comment

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

s/, _//

if requiresCertificate(service) {
ports, err := getSSLEnabledPorts(service)
if err != nil {
return nil, err
}
ssl = NewSSLConfig(lbName, ports, cp)
secretListenerString, _ := service.Annotations[ServiceAnnotationLoadBalancerTLSSecret]
secretBackendSetString, _ := service.Annotations[ServiceAnnotationLoadBalancerBackendSetSecret]
Copy link
Contributor

Choose a reason for hiding this comment

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

s/, _//

}
if cert == "" || key == "" {
if len(sslSecret.PublicCert) == 0 || len(sslSecret.PrivateKey) == 0 {
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 clear on how this works in the case that there's BackendSet certs but not Listener?

Copy link
Member Author

@MadalinaPatrichi MadalinaPatrichi Sep 19, 2018

Choose a reason for hiding this comment

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

LMAATCFT (let me add a test case for that) good spot

secretName = sslCfg.BackendSetSSLSecretName
}
sslConfig := getSSLConfiguration(sslCfg, secretName, port)
if sslConfig != nil {
Copy link
Contributor

Choose a reason for hiding this comment

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

No need for if statement. loadbalancer.BackendSetDetails{SslConfiguration: sslConfig} when sslConfig == nil is equivalent to not specifying the property as the default value for pointers is nil.

if len(sslSecret.PublicCert) == 0 || len(sslSecret.PrivateKey) == 0 {
return certs, nil
}
if len(s.SSLConfig.ListenerSSLSecretName) != 0 {
Copy link
Contributor

Choose a reason for hiding this comment

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

We can support ca certificates and passphrases on listener certs and implement as follows I believe:

	secrets := make([]string, 0, 2)
	if s.SSLConfig.ListenerSSLSecretName != "" {
		secrets = append(secrets, s.SSLConfig.ListenerSSLSecretName)
	}
	if s.SSLConfig.BackendSetSSLSecretName != "" {
		secrets = append(secrets, s.SSLConfig.BackendSetSSLSecretName)
	}

	for _, name := range secrets {
		cert, err := s.SSLConfig.readSSLSecret(s.service.Namespace, name)
		if err != nil {
			return nil, errors.Wrap(err, "reading SSL BackendSet Secret")
		}

		certs[name] = loadbalancer.CertificateDetails{
			CertificateName:   &s.SSLConfig.BackendSetSSLSecretName,
			CaCertificate:     common.String(string(cert.CACert)),
			PublicCertificate: common.String(string(cert.PublicCert)),
			PrivateKey:        common.String(string(cert.PrivateKey)),
			Passphrase:        common.String(string(cert.Passphrase)),
		}
	}

@MadalinaPatrichi MadalinaPatrichi force-pushed the mp/TLSImplementation branch 5 times, most recently from ef38a27 to 5d97908 Compare September 20, 2018 13:31
Copy link
Member

@owainlewis owainlewis left a comment

Choose a reason for hiding this comment

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

Couple of minor comments to address from previous reviews. Overall looks good to me.

annotations:
service.beta.kubernetes.io/oci-load-balancer-ssl-ports: "443"
service.beta.kubernetes.io/oci-load-balancer-tls-secret: ssl-certificate-secret
service.beta.kubernetes.io/oci-load-balancer-backendset-secret: ssl-certificate-secret
Copy link
Member

Choose a reason for hiding this comment

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

To be consistent let's change to service.beta.kubernetes.io/oci-load-balancer-tls-backendset-secret

tcpIngressIP := framework.GetIngressPoint(&tcpService.Status.LoadBalancer.Ingress[0])
framework.Logf("TCP load balancer: %s", tcpIngressIP)

// By("hitting the TCP service's NodePort")
Copy link
Member

Choose a reason for hiding this comment

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

Let's remove unused code

// specifying the generic secret to install on the load balancer listeners which
// have SSL enabled.
// See: https://kubernetes.io/docs/concepts/services-networking/ingress/#tls
ServiceAnnotationLoadBalancerBackendSetSecret = "service.beta.kubernetes.io/oci-load-balancer-backendset-secret"
Copy link
Member

Choose a reason for hiding this comment

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

service.beta.kubernetes.io/oci-load-balancer-backendset-secret => service.beta.kubernetes.io/oci-load-balancer-backendset-tls-secret to be consistent?

var cert, key []byte
if cert, ok = secret.Data[sslCertificateFileName]; !ok {
return "", "", errors.Errorf("%s not found in secret %s/%s", sslCertificateFileName, ns, name)
var ok bool
Copy link
Member

Choose a reason for hiding this comment

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

Is this (var ok bool) needed?

if cert, ok := secret.Data[SSLCertificateFileName]; !ok {

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, I think it is. It's because we need to declare cert and key beforehand.

certs, err := spec.Certificates()
if err != nil {
return err
}

var ok bool
Copy link
Member

Choose a reason for hiding this comment

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

Is this needed in outer scope vs if _, ok := lb.Certificates[*cert.CertificateName]; !ok {

ns := f.Namespace.Name

jig := framework.NewServiceTestJig(f.ClientSet, serviceName)
//nodeIP := framework.PickNodeIP(jig.Client) // for later
Copy link
Member

Choose a reason for hiding this comment

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

Remove unused code

jig := framework.NewServiceTestJig(f.ClientSet, serviceName)
//nodeIP := framework.PickNodeIP(jig.Client) // for later

sslSecretNameL := "ssl-certificate-secret-listener"
Copy link
Member

Choose a reason for hiding this comment

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

Minor: Would generally prefer a more readable name like sslListenerSecretName

cloudprovider.SSLPassphrase: []byte(framework.SSLPassphrase),
},
})
sslSecretNameB := "ssl-certificate-secret-backendset"
Copy link
Member

Choose a reason for hiding this comment

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

Minor: Would prefer sslBackendSetSecretName vs sslSecretNameB

loadBalancerCreateTimeout = framework.LoadBalancerCreateTimeoutLarge
}

// TODO(apryde): Test that LoadBalancers can receive static IP addresses
Copy link
Member

Choose a reason for hiding this comment

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

Remove me

loadBalancerCreateTimeout = framework.LoadBalancerCreateTimeoutLarge
}

// TODO(apryde): Test that LoadBalancers can receive static IP addresses
Copy link
Member

Choose a reason for hiding this comment

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

Copy paste from previous test. Probably not worth duplicating again.

Copy link
Member

@owainlewis owainlewis left a comment

Choose a reason for hiding this comment

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

Looks good. Once additional reviews are +1 we can merge.

@MadalinaPatrichi MadalinaPatrichi force-pushed the mp/TLSImplementation branch 4 times, most recently from 8e313ba to b64268e Compare September 21, 2018 15:03
Copy link
Contributor

@prydie prydie left a comment

Choose a reason for hiding this comment

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

Awesome work! 🎉

@prydie prydie merged commit 1da23ec into master Sep 21, 2018
@prydie prydie deleted the mp/TLSImplementation branch September 21, 2018 16:25
@prydie prydie added this to the 0.7.0 milestone Sep 21, 2018
ayushverma14 pushed a commit to ayushverma14/oci-cloud-controller-manager that referenced this pull request Jul 18, 2022
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants