Skip to content
This repository was archived by the owner on Apr 17, 2019. It is now read-only.

Commit 3cd193a

Browse files
committed
SSL termination support with HAProxy
1 parent f325e69 commit 3cd193a

13 files changed

+236
-43
lines changed

hack/verify-flags/known-flags.txt

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -59,4 +59,5 @@ whitelist-override-label
5959
configuration-name
6060
custom-error-service
6161
use-unicast
62-
62+
ssl-ca-cert
63+
ssl-cert

service-loadbalancer/README.md

Lines changed: 52 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -115,6 +115,56 @@ A couple of points to note:
115115
- The https service is accessible directly on the specified port, which matches the *service port*.
116116
- You need to take care of ensuring there is no collision between these service ports on the node.
117117

118+
#### SSL Termination
119+
120+
- Annotate a service
121+
122+
```yaml
123+
metadata:
124+
name: myservice
125+
annotations:
126+
serviceloadbalancer/lb.sslTerm: "true"
127+
serviceloadbalancer/lb.aclMatch: "-i /t1 /t2"
128+
labels:
129+
```
130+
131+
- Create a secret with one of your favorite tool
132+
133+
- Add secrets to your loadbalancer pod
134+
135+
```yaml
136+
ports:
137+
# All http services
138+
- containerPort: 80
139+
hostPort: 80
140+
protocol: TCP
141+
# ssl term
142+
- containerPort: 443
143+
hostPort: 443
144+
protocol: TCP
145+
# haproxy stats
146+
- containerPort: 1936
147+
hostPort: 1936
148+
protocol: TCP
149+
resources: {}
150+
volumes:
151+
- name: secret-volume
152+
secret:
153+
secretName: my-secret
154+
volumeMounts:
155+
- mountPath: "/ssl"
156+
name: secret-volume
157+
```
158+
159+
- Add your SSL configuration to loadbalancer pod
160+
161+
```yaml
162+
args:
163+
- --ssl-cert=/ssl/crt.pem
164+
- --ssl-ca-cert=/ssl/ca.crt
165+
- --namespace=default
166+
```
167+
118168
#### TCP
119169
120170
```yaml
@@ -325,9 +375,8 @@ status of the services or stats about the traffic
325375
- Scrape :1926 and scale replica count of the loadbalancer rc from a helper pod (this is basically ELB)
326376
- Scrape :1936/;csv and autoscale services
327377
- Better https support. 3 options to handle ssl:
328-
1. __Termination__: certificate lives on load balancer. All traffic to load balancer is encrypted, traffic from load balancer to service is not.
329-
2. __Pass Through__: Load balancer drops down to L4 balancing and forwards TCP encrypted packets to destination.
330-
3. __Redirect__: All traffic is https. HTTP connections are encrypted using load balancer certs.
378+
1. __Pass Through__: Load balancer drops down to L4 balancing and forwards TCP encrypted packets to destination.
379+
2. __Redirect__: All traffic is https. HTTP connections are encrypted using load balancer certs.
331380

332381
Currently you need to trigger TCP loadbalancing for your https service by specifying it in loadbalancer.json. Support for the other 2 would be nice.
333382
- Multinamespace support: Currently the controller only watches a single namespace for services.

service-loadbalancer/loadbalancer.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,3 +4,4 @@
44
"config": "/etc/haproxy/haproxy.cfg",
55
"template": "template.cfg"
66
}
7+

service-loadbalancer/service_loadbalancer.go

Lines changed: 80 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,8 @@ const (
5050
lbApiPort = 8081
5151
lbAlgorithmKey = "serviceloadbalancer/lb.algorithm"
5252
lbHostKey = "serviceloadbalancer/lb.host"
53+
lbSslTerm = "serviceloadbalancer/lb.sslTerm"
54+
lbAclMatch = "serviceloadbalancer/lb.aclMatch"
5355
lbCookieStickySessionKey = "serviceloadbalancer/lb.cookie-sticky-session"
5456
defaultErrorPage = "file:///etc/haproxy/errors/404.http"
5557
)
@@ -72,31 +74,31 @@ var (
7274
supportedAlgorithms = []string{"roundrobin", "leastconn", "first", "source"}
7375

7476
config = flags.String("cfg", "loadbalancer.json", `path to load balancer json config.
75-
Note that this is *not* the path to the configuration file for the load balancer
76-
itself, but rather, the path to the json configuration of how you would like the
77-
load balancer to behave in the kubernetes cluster.`)
77+
Note that this is *not* the path to the configuration file for the load balancer
78+
itself, but rather, the path to the json configuration of how you would like the
79+
load balancer to behave in the kubernetes cluster.`)
7880

7981
dry = flags.Bool("dry", false, `if set, a single dry run of configuration
80-
parsing is executed. Results written to stdout.`)
82+
parsing is executed. Results written to stdout.`)
8183

8284
cluster = flags.Bool("use-kubernetes-cluster-service", true, `If true, use the built in kubernetes
83-
cluster for creating the client`)
85+
cluster for creating the client`)
8486

8587
// If you have pure tcp services or https services that need L3 routing, you
8688
// must specify them by name. Note that you are responsible for:
8789
// 1. Making sure there is no collision between the service ports of these services.
88-
// - You can have multiple <mysql svc name>:3306 specifications in this map, and as
89-
// long as the service ports of your mysql service don't clash, you'll get
90-
// loadbalancing for each one.
90+
// - You can have multiple <mysql svc name>:3306 specifications in this map, and as
91+
// long as the service ports of your mysql service don't clash, you'll get
92+
// loadbalancing for each one.
9193
// 2. Exposing the service ports as node ports on a pod.
9294
// 3. Adding firewall rules so these ports can ingress traffic.
9395
//
9496
// Any service not specified in this map is treated as an http:80 service,
9597
// unless TargetService dictates otherwise.
9698

9799
tcpServices = flags.String("tcp-services", "", `Comma separated list of tcp/https
98-
serviceName:servicePort pairings. This assumes you've opened up the right
99-
hostPorts for each service that serves ingress traffic.`)
100+
serviceName:servicePort pairings. This assumes you've opened up the right
101+
hostPorts for each service that serves ingress traffic.`)
100102

101103
targetService = flags.String(
102104
"target-service", "", `Restrict loadbalancing to a single target service.`)
@@ -115,21 +117,25 @@ var (
115117
// backend svc_p2: pod1:tp2, pod2:tp2
116118

117119
forwardServices = flags.Bool("forward-services", false, `Forward to service vip
118-
instead of endpoints. This will use kube-proxy's inbuilt load balancing.`)
120+
instead of endpoints. This will use kube-proxy's inbuilt load balancing.`)
119121

120122
httpPort = flags.Int("http-port", 80, `Port to expose http services.`)
121123
statsPort = flags.Int("stats-port", 1936, `Port for loadbalancer stats,
122-
Used in the loadbalancer liveness probe.`)
124+
Used in the loadbalancer liveness probe.`)
123125

124126
startSyslog = flags.Bool("syslog", false, `if set, it will start a syslog server
125-
that will forward haproxy logs to stdout.`)
127+
that will forward haproxy logs to stdout.`)
128+
129+
sslCert = flags.String("ssl-cert", "", `if set, it will load the certificate.`)
130+
sslCaCert = flags.String("ssl-ca-cert", "", `if set, it will load the certificate from which
131+
to load CA certificates used to verify client's certificate.`)
126132

127133
errorPage = flags.String("error-page", "", `if set, it will try to load the content
128-
as a web page and use the content as error page. Is required that the URL returns
129-
200 as a status code`)
134+
as a web page and use the content as error page. Is required that the URL returns
135+
200 as a status code`)
130136

131137
lbDefAlgorithm = flags.String("balance-algorithm", "roundrobin", `if set, it allows a custom
132-
default balance algorithm.`)
138+
default balance algorithm.`)
133139
)
134140

135141
// service encapsulates a single backend entry in the load balancer config.
@@ -152,6 +158,12 @@ type service struct {
152158
// host header inside the http request. It only applies to http traffic.
153159
Host string
154160

161+
// if true, terminate ssl using the loadbalancers certificates.
162+
SslTerm bool
163+
164+
// if set use this to match the path rule
165+
AclMatch string
166+
155167
// Algorithm
156168
Algorithm string
157169

@@ -193,6 +205,8 @@ type loadBalancerConfig struct {
193205
Template string `json:"template" description:"template for the load balancer config."`
194206
Algorithm string `json:"algorithm" description:"loadbalancing algorithm."`
195207
startSyslog bool `description:"indicates if the load balancer uses syslog."`
208+
sslCert string `json:"sslCert" description:"PEM for ssl."`
209+
sslCaCert string `json:"sslCaCert" description:"PEM to verify client's certificate."`
196210
lbDefAlgorithm string `description:"custom default load balancer algorithm".`
197211
}
198212

@@ -219,6 +233,16 @@ func (s serviceAnnotations) getCookieStickySession() (string, bool) {
219233
return val, ok
220234
}
221235

236+
func (s serviceAnnotations) getSslTerm() (string, bool) {
237+
val, ok := s[lbSslTerm]
238+
return val, ok
239+
}
240+
241+
func (s serviceAnnotations) getAclMatch() (string, bool) {
242+
val, ok := s[lbAclMatch]
243+
return val, ok
244+
}
245+
222246
// Get serves the error page
223247
func (s *staticPageHandler) Getfunc(w http.ResponseWriter, r *http.Request) {
224248
w.WriteHeader(404)
@@ -277,6 +301,15 @@ func (cfg *loadBalancerConfig) write(services map[string][]service, dryRun bool)
277301
conf["startSyslog"] = strconv.FormatBool(cfg.startSyslog)
278302
conf["services"] = services
279303

304+
var sslConfig string
305+
if cfg.sslCert != "" {
306+
sslConfig = "crt " + cfg.sslCert
307+
}
308+
if cfg.sslCaCert != "" {
309+
sslConfig += " ca-file " + cfg.sslCaCert
310+
}
311+
conf["sslCert"] = sslConfig
312+
280313
// default load balancer algorithm is roundrobin
281314
conf["defLbAlgorithm"] = lbDefAlgorithm
282315
if cfg.lbDefAlgorithm != "" {
@@ -367,7 +400,7 @@ func getServiceNameForLBRule(s *api.Service, servicePort int) string {
367400
}
368401

369402
// getServices returns a list of services and their endpoints.
370-
func (lbc *loadBalancerController) getServices() (httpSvc []service, tcpSvc []service) {
403+
func (lbc *loadBalancerController) getServices() (httpSvc []service, httpsTermSvc []service, tcpSvc []service) {
371404
ep := []string{}
372405
services, _ := lbc.svcLister.List()
373406
for _, s := range services.Items {
@@ -422,6 +455,19 @@ func (lbc *loadBalancerController) getServices() (httpSvc []service, tcpSvc []se
422455
newSvc.SessionAffinity = true
423456
}
424457

458+
// By default sslTerm is disabled
459+
newSvc.SslTerm = false
460+
if val, ok := serviceAnnotations(s.ObjectMeta.Annotations).getSslTerm(); ok {
461+
b, err := strconv.ParseBool(val)
462+
if err == nil {
463+
newSvc.SslTerm = b
464+
}
465+
}
466+
467+
if val, ok := serviceAnnotations(s.ObjectMeta.Annotations).getAclMatch(); ok {
468+
newSvc.AclMatch = val
469+
}
470+
425471
if port, ok := lbc.tcpServices[sName]; ok && port == servicePort.Port {
426472
newSvc.FrontendPort = servicePort.Port
427473
tcpSvc = append(tcpSvc, newSvc)
@@ -434,13 +480,18 @@ func (lbc *loadBalancerController) getServices() (httpSvc []service, tcpSvc []se
434480
}
435481

436482
newSvc.FrontendPort = lbc.httpPort
437-
httpSvc = append(httpSvc, newSvc)
483+
if newSvc.SslTerm == true {
484+
httpsTermSvc = append(httpsTermSvc, newSvc)
485+
} else {
486+
httpSvc = append(httpSvc, newSvc)
487+
}
438488
}
439489
glog.Infof("Found service: %+v", newSvc)
440490
}
441491
}
442492

443493
sort.Sort(serviceByName(httpSvc))
494+
sort.Sort(serviceByName(httpsTermSvc))
444495
sort.Sort(serviceByName(tcpSvc))
445496

446497
return
@@ -452,18 +503,15 @@ func (lbc *loadBalancerController) sync(dryRun bool) error {
452503
time.Sleep(100 * time.Millisecond)
453504
return errDeferredSync
454505
}
455-
456-
lbc.reloadRateLimiter.Accept()
457-
458-
httpSvc, tcpSvc := lbc.getServices()
459-
if len(httpSvc) == 0 && len(tcpSvc) == 0 {
460-
glog.Info("There is no available services to be used in the load balancer")
506+
httpSvc, httpsTermSvc, tcpSvc := lbc.getServices()
507+
if len(httpSvc) == 0 && len(httpsTermSvc) == 0 && len(tcpSvc) == 0 {
461508
return nil
462509
}
463510
if err := lbc.cfg.write(
464511
map[string][]service{
465-
"http": httpSvc,
466-
"tcp": tcpSvc,
512+
"http": httpSvc,
513+
"httpsTerm": httpsTermSvc,
514+
"tcp": tcpSvc,
467515
}, dryRun); err != nil {
468516
return err
469517
}
@@ -533,7 +581,7 @@ func newLoadBalancerController(cfg *loadBalancerConfig, kubeClient *unversioned.
533581

534582
// parseCfg parses the given configuration file.
535583
// cmd line params take precedence over config directives.
536-
func parseCfg(configPath string, defLbAlgorithm string) *loadBalancerConfig {
584+
func parseCfg(configPath string, defLbAlgorithm string, sslCert string, sslCaCert string) *loadBalancerConfig {
537585
jsonBlob, err := ioutil.ReadFile(configPath)
538586
if err != nil {
539587
glog.Fatalf("Could not parse lb config: %v", err)
@@ -543,7 +591,8 @@ func parseCfg(configPath string, defLbAlgorithm string) *loadBalancerConfig {
543591
if err != nil {
544592
glog.Fatalf("Unable to unmarshal json blob: %v", string(jsonBlob))
545593
}
546-
594+
cfg.sslCert = sslCert
595+
cfg.sslCaCert = sslCaCert
547596
cfg.lbDefAlgorithm = defLbAlgorithm
548597
glog.Infof("Creating new loadbalancer: %+v", cfg)
549598
return &cfg
@@ -612,7 +661,7 @@ func dryRun(lbc *loadBalancerController) {
612661
func main() {
613662
clientConfig := kubectl_util.DefaultClientConfig(flags)
614663
flags.Parse(os.Args)
615-
cfg := parseCfg(*config, *lbDefAlgorithm)
664+
cfg := parseCfg(*config, *lbDefAlgorithm, *sslCert, *sslCaCert)
616665

617666
var kubeClient *unversioned.Client
618667
var err error
@@ -660,6 +709,7 @@ func main() {
660709

661710
// TODO: Handle multiple namespaces
662711
lbc := newLoadBalancerController(cfg, kubeClient, namespace, tcpSvcs)
712+
663713
go lbc.epController.Run(util.NeverStop)
664714
go lbc.svcController.Run(util.NeverStop)
665715
if *dry {
@@ -668,4 +718,5 @@ func main() {
668718
lbc.cfg.reload()
669719
util.Until(lbc.worker, time.Second, util.NeverStop)
670720
}
721+
671722
}

0 commit comments

Comments
 (0)