Skip to content

Commit 1da23ec

Browse files
MadalinaPatrichiprydie
authored andcommitted
Support assigning SSL certs to BackendSets (#243)
1 parent 3d6ae4d commit 1da23ec

File tree

11 files changed

+674
-124
lines changed

11 files changed

+674
-124
lines changed

docs/tutorial-ssl-backendset.md

Lines changed: 105 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,105 @@
1+
# Tutorial
2+
3+
This example will show you how to use the CCM to create a load balancer with SSL
4+
termination for BackendSets.
5+
6+
### Load balancer with SSL termination for BackendSets example
7+
8+
When you create a service with --type=LoadBalancer a OCI load balancer will be
9+
created.
10+
11+
The example below will create an NGINX deployment and expose it via a load
12+
balancer serving http on port 80, and https on 443. Note that the service
13+
**type** is set to **LoadBalancer**.
14+
15+
```yaml
16+
---
17+
apiVersion: apps/v1beta1
18+
kind: Deployment
19+
metadata:
20+
name: nginx-deployment
21+
spec:
22+
replicas: 2
23+
template:
24+
metadata:
25+
labels:
26+
app: nginx
27+
spec:
28+
containers:
29+
- name: nginx
30+
image: nginx
31+
ports:
32+
- containerPort: 80
33+
---
34+
kind: Service
35+
apiVersion: v1
36+
metadata:
37+
name: nginx-service
38+
annotations:
39+
service.beta.kubernetes.io/oci-load-balancer-ssl-ports: "443"
40+
service.beta.kubernetes.io/oci-load-balancer-tls-secret: ssl-certificate-secret
41+
service.beta.kubernetes.io/oci-load-balancer-tls-backendset-secret: ssl-certificate-secret
42+
spec:
43+
selector:
44+
app: nginx
45+
type: LoadBalancer
46+
ports:
47+
- name: http
48+
port: 80
49+
targetPort: 80
50+
- name: https
51+
port: 443
52+
targetPort: 80
53+
```
54+
55+
First, the required Secret needs to be created in Kubernetes. For the purposes
56+
of this example, we will create a self-signed certificate. However, in
57+
production you would most likely use a public certificate signed by a
58+
certificate authority.
59+
60+
Below is an example of a secret configuration file required to be uploaded as a Kubernetes
61+
generic Secret. The CA certificate, the public certificate and the private key need to be base64 encoded:
62+
63+
***Note: Certificates for BackendSets require a CA certificate to be provided.***
64+
65+
```yaml
66+
apiVersion: v1
67+
kind: Secret
68+
metadata:
69+
name: ssl-certificate-secret
70+
type: Opaque
71+
data:
72+
ca.crt: LS0tLS1CRUdJTiBDRV(...)
73+
tls.crt: LS0tLS1CRUdJTi(...)
74+
tls.key: LS0tLS1CRUdJTi(...)
75+
```
76+
77+
```
78+
kubectl create -f ssl-certificate-secret.yaml
79+
```
80+
81+
Create the service:
82+
83+
```
84+
$ kubectl create -f manifests/demo/nginx-demo-svc-ssl.yaml
85+
```
86+
87+
Watch the service and await a public IP address. This will be the load balancer
88+
IP which you can use to connect to your service.
89+
90+
```
91+
$ kubectl get svc --watch
92+
NAME CLUSTER-IP EXTERNAL-IP PORT(S) AGE
93+
nginx-service 10.96.97.137 129.213.12.174 80:30274/TCP 5m
94+
```
95+
96+
You can now access your service via the provisioned load balancer using either
97+
http or https:
98+
99+
```
100+
curl http://129.213.12.174
101+
curl --insecure https://129.213.12.174
102+
```
103+
104+
Note: The `--insecure` flag above is only required due to our use of self-signed
105+
certificates in this example.

pkg/oci/client/load_balancer.go

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@ type LoadBalancerInterface interface {
3535
DeleteLoadBalancer(ctx context.Context, id string) (string, error)
3636

3737
GetCertificateByName(ctx context.Context, lbID, name string) (*loadbalancer.Certificate, error)
38-
CreateCertificate(ctx context.Context, lbID, certificate, key string) (string, error)
38+
CreateCertificate(ctx context.Context, lbID string, cert loadbalancer.CertificateDetails) (string, error)
3939

4040
CreateBackendSet(ctx context.Context, lbID, name string, details loadbalancer.BackendSetDetails) (string, error)
4141
UpdateBackendSet(ctx context.Context, lbID, name string, details loadbalancer.BackendSetDetails) (string, error)
@@ -150,18 +150,19 @@ func (c *client) GetCertificateByName(ctx context.Context, lbID, name string) (*
150150
return nil, errors.WithStack(errNotFound)
151151
}
152152

153-
func (c *client) CreateCertificate(ctx context.Context, lbID, certificate, key string) (string, error) {
153+
func (c *client) CreateCertificate(ctx context.Context, lbID string, cert loadbalancer.CertificateDetails) (string, error) {
154154
if !c.rateLimiter.Writer.TryAccept() {
155155
return "", RateLimitError(true, "CreateCertificate")
156156
}
157157

158-
// TODO(apryde): We currently don't have a mechanism for supplying
159-
// CreateCertificateDetails.CaCertificate.
160158
resp, err := c.loadbalancer.CreateCertificate(ctx, loadbalancer.CreateCertificateRequest{
161159
LoadBalancerId: &lbID,
162160
CreateCertificateDetails: loadbalancer.CreateCertificateDetails{
163-
PublicCertificate: &certificate,
164-
PrivateKey: &key,
161+
CertificateName: cert.CertificateName,
162+
CaCertificate: cert.CaCertificate,
163+
PublicCertificate: cert.PublicCertificate,
164+
PrivateKey: cert.PrivateKey,
165+
Passphrase: cert.Passphrase,
165166
},
166167
})
167168
incRequestCounter(err, createVerb, certificateResource)

pkg/oci/load_balancer.go

Lines changed: 44 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,12 @@ const (
6363
// See: https://kubernetes.io/docs/concepts/services-networking/ingress/#tls
6464
ServiceAnnotationLoadBalancerTLSSecret = "service.beta.kubernetes.io/oci-load-balancer-tls-secret"
6565

66+
// ServiceAnnotationLoadBalancerTLSBackendSetSecret is a Service annotation for
67+
// specifying the generic secret to install on the load balancer listeners which
68+
// have SSL enabled.
69+
// See: https://kubernetes.io/docs/concepts/services-networking/ingress/#tls
70+
ServiceAnnotationLoadBalancerTLSBackendSetSecret = "service.beta.kubernetes.io/oci-load-balancer-tls-backendset-secret"
71+
6672
// ServiceAnnotationLoadBalancerConnectionIdleTimeout is the annotation used
6773
// on the service to specify the idle connection timeout.
6874
ServiceAnnotationLoadBalancerConnectionIdleTimeout = "service.beta.kubernetes.io/oci-load-balancer-connection-idle-timeout"
@@ -89,9 +95,10 @@ const (
8995
// Fallback value if annotation on service is not set
9096
lbDefaultShape = "100Mbps"
9197

92-
lbNodesHealthCheckPath = "/healthz"
93-
lbNodesHealthCheckPort = k8sports.ProxyHealthzPort
94-
lbNodesHealthCheckProto = "HTTP"
98+
lbNodesHealthCheckPath = "/healthz"
99+
lbNodesHealthCheckPort = k8sports.ProxyHealthzPort
100+
lbNodesHealthCheckProtoHTTP = "HTTP"
101+
lbNodesHealthCheckProtoTCP = "TCP"
95102
)
96103

97104
// GetLoadBalancer returns whether the specified load balancer exists, and if
@@ -189,62 +196,48 @@ func getSubnetsForNodes(ctx context.Context, nodes []*v1.Node, client client.Int
189196

190197
// readSSLSecret returns the certificate and private key from a Kubernetes TLS
191198
// private key Secret.
192-
func (cp *CloudProvider) readSSLSecret(svc *v1.Service) (string, string, error) {
193-
secretString, ok := svc.Annotations[ServiceAnnotationLoadBalancerTLSSecret]
194-
if !ok {
195-
return "", "", errors.Errorf("no %q annotation found", ServiceAnnotationLoadBalancerTLSSecret)
196-
}
197-
198-
ns, name := parseSecretString(secretString)
199-
if ns == "" {
200-
ns = svc.Namespace
201-
}
199+
func (cp *CloudProvider) readSSLSecret(ns, name string) (*certificateData, error) {
202200
secret, err := cp.kubeclient.CoreV1().Secrets(ns).Get(name, metav1.GetOptions{})
203201
if err != nil {
204-
return "", "", err
202+
return nil, err
205203
}
206-
207-
var cert, key []byte
208-
if cert, ok = secret.Data[sslCertificateFileName]; !ok {
209-
return "", "", errors.Errorf("%s not found in secret %s/%s", sslCertificateFileName, ns, name)
204+
var ok bool
205+
var cacert, cert, key, pass []byte
206+
cacert = secret.Data[SSLCAFileName]
207+
if cert, ok = secret.Data[SSLCertificateFileName]; !ok {
208+
return nil, errors.Errorf("%s not found in secret %s/%s", SSLCertificateFileName, ns, name)
210209
}
211-
if key, ok = secret.Data[sslPrivateKeyFileName]; !ok {
212-
return "", "", errors.Errorf("%s not found in secret %s/%s", sslPrivateKeyFileName, ns, name)
210+
if key, ok = secret.Data[SSLPrivateKeyFileName]; !ok {
211+
return nil, errors.Errorf("%s not found in secret %s/%s", SSLPrivateKeyFileName, ns, name)
213212
}
214-
215-
return string(cert), string(key), nil
213+
pass = secret.Data[SSLPassphrase]
214+
return &certificateData{CACert: cacert, PublicCert: cert, PrivateKey: key, Passphrase: pass}, nil
216215
}
217216

218217
// ensureSSLCertificate creates a OCI SSL certificate to the given load
219218
// balancer, if it doesn't already exist.
220-
func (cp *CloudProvider) ensureSSLCertificate(ctx context.Context, lb *loadbalancer.LoadBalancer, spec *LBSpec) error {
221-
name := spec.SSLConfig.Name
222-
logger := cp.logger.With("loadBalancerID", *lb.Id, "certificateName", name)
223-
_, err := cp.client.LoadBalancer().GetCertificateByName(ctx, *lb.Id, name)
224-
if err == nil {
225-
logger.Debug("Certificate already exists on load balancer. Nothing to do.")
226-
return nil
227-
}
228-
if !client.IsNotFound(err) {
229-
return err
230-
}
231-
232-
// Although we iterate here only one certificate is supported at the moment.
219+
func (cp *CloudProvider) ensureSSLCertificates(ctx context.Context, lb *loadbalancer.LoadBalancer, spec *LBSpec) error {
220+
logger := cp.logger.With("loadBalancerID", *lb.Id)
221+
// Get all required certificates
233222
certs, err := spec.Certificates()
234223
if err != nil {
235224
return err
236225
}
226+
237227
for _, cert := range certs {
238-
wrID, err := cp.client.LoadBalancer().CreateCertificate(ctx, *lb.Id, *cert.PublicCertificate, *cert.PrivateKey)
239-
if err != nil {
240-
return err
241-
}
242-
_, err = cp.client.LoadBalancer().AwaitWorkRequest(ctx, wrID)
243-
if err != nil {
244-
return err
245-
}
228+
if _, ok := lb.Certificates[*cert.CertificateName]; !ok {
229+
logger = cp.logger.With("certificateName", *cert.CertificateName)
230+
wrID, err := cp.client.LoadBalancer().CreateCertificate(ctx, *lb.Id, cert)
231+
if err != nil {
232+
return err
233+
}
234+
_, err = cp.client.LoadBalancer().AwaitWorkRequest(ctx, wrID)
235+
if err != nil {
236+
return err
237+
}
246238

247-
logger.Info("Certificate created")
239+
logger.Info("Certificate created")
240+
}
248241
}
249242
return nil
250243
}
@@ -323,16 +316,18 @@ func (cp *CloudProvider) EnsureLoadBalancer(ctx context.Context, clusterName str
323316
}
324317
exists := !client.IsNotFound(err)
325318

326-
var ssl *SSLConfig
319+
var sslConfig *SSLConfig
327320
if requiresCertificate(service) {
328321
ports, err := getSSLEnabledPorts(service)
329322
if err != nil {
330323
return nil, err
331324
}
332-
ssl = NewSSLConfig(lbName, ports, cp)
325+
secretListenerString := service.Annotations[ServiceAnnotationLoadBalancerTLSSecret]
326+
secretBackendSetString := service.Annotations[ServiceAnnotationLoadBalancerTLSBackendSetSecret]
327+
sslConfig = NewSSLConfig(secretListenerString, secretBackendSetString, ports, cp)
333328
}
334329
subnets := []string{cp.config.LoadBalancer.Subnet1, cp.config.LoadBalancer.Subnet2}
335-
spec, err := NewLBSpec(service, nodes, subnets, ssl, cp.securityListManagerFactory)
330+
spec, err := NewLBSpec(service, nodes, subnets, sslConfig, cp.securityListManagerFactory)
336331
if err != nil {
337332
logger.With(zap.Error(err)).Error("Failed to derive LBSpec")
338333
return nil, err
@@ -351,8 +346,8 @@ func (cp *CloudProvider) EnsureLoadBalancer(ctx context.Context, clusterName str
351346

352347
// If the load balancer needs an SSL cert ensure it is present.
353348
if requiresCertificate(service) {
354-
if err := cp.ensureSSLCertificate(ctx, lb, spec); err != nil {
355-
return nil, errors.Wrap(err, "ensuring ssl certificate")
349+
if err := cp.ensureSSLCertificates(ctx, lb, spec); err != nil {
350+
return nil, errors.Wrap(err, "ensuring ssl certificates")
356351
}
357352
}
358353

0 commit comments

Comments
 (0)