Merge pull request #873 from influxdata/feature/tls

Add TLS options to server
pull/10616/head
Chris Goller 2017-02-16 09:08:43 -06:00 committed by GitHub
commit d6a283b289
5 changed files with 141 additions and 13 deletions

View File

@ -3,6 +3,7 @@
### Bug Fixes
### Features
1. [#873](https://github.com/influxdata/chronograf/pull/873): Add [TLS](https://github.com/influxdata/chronograf/blob/master/docs/tls.md) support
### UI Improvements

View File

@ -107,6 +107,9 @@ A UI for [Kapacitor](https://github.com/influxdata/kapacitor) alert creation and
* View all active alerts at a glance on the alerting dashboard
* Enable and disable existing alert rules with the check of a box
### TLS/HTTPS support
See [Chronograf with TLS](https://github.com/influxdata/chronograf/blob/master/docs/tls.md) for more information.
### GitHub OAuth Login
See [Chronograf with OAuth 2.0](https://github.com/influxdata/chronograf/blob/master/docs/auth.md) for more information.

69
docs/tls.md Normal file
View File

@ -0,0 +1,69 @@
## Chronograf TLS
Chronograf supports TLS to securely communicate between the browser and server via
HTTPS.
We recommend using HTTPS with Chronograf. If you are not using a TLS termination proxy,
you can run Chronograf's server with TLS connections.
### TL;DR
```sh
chronograf --cert=my.crt --key=my.key
```
### Running Chronograf with TLS
Chronograf server has command line and environment variable options to specify
the certificate and key files. The server reads and parses a public/private key
pair from these files. The files must contain PEM encoded data.
In Chronograf all command line options also have a corresponding environment
variable.
To specify the certificate file either use the `--cert` CLI option or `TLS_CERTIFICATE`
environment variable.
To specify the key file either use the `--key` CLI option or `TLS_PRIVATE_KEY`
environment variable.
To specify the certificate and key if both are in the same file either use the `--cert`
CLI option or `TLS_CERTIFICATE` environment variable.
#### Example with CLI options
```sh
chronograf --cert=my.crt --key=my.key
```
#### Example with environment variables
```sh
TLS_CERTIFICATE=my.crt TLS_PRIVATE_KEY=my.key chronograf
```
#### Docker example with environment variables
```sh
docker run -v /host/path/to/certs:/certs -e TLS_CERTIFICATE=/certs/my.crt -e TLS_PRIVATE_KEY=/certs/my.key quay.io/influxdb/chronograf:latest
```
### Testing with self-signed certificates
In a production environment you should not use self-signed certificates. However,
for testing it is fast to create your own certs.
To create a cert and key in one file with openssl:
```sh
openssl req -x509 -newkey rsa:4096 -sha256 -nodes -keyout testing.pem -out testing.pem -subj "/CN=localhost" -days 365
```
Next, set the environment variable `TLS_CERTIFICATE`:
```sh
export TLS_CERTIFICATE=$PWD/testing.pem
```
Run chronograf:
```sh
./chronograf
INFO[0000] Serving chronograf at https://[::]:8888 component=server
```
In the first log message you should see `https` rather than `http`.

12
server/hsts.go Normal file
View File

@ -0,0 +1,12 @@
package server
import "net/http"
// HSTS add HTTP Strict Transport Security header with a max-age of two years
// Inspired from https://blog.bracebin.com/achieving-perfect-ssl-labs-score-with-go
func HSTS(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Header().Add("Strict-Transport-Security", "max-age=63072000; includeSubDomains")
next.ServeHTTP(w, r)
})
}

View File

@ -1,6 +1,7 @@
package server
import (
"crypto/tls"
"math/rand"
"net"
"net/http"
@ -16,6 +17,7 @@ import (
clog "github.com/influxdata/chronograf/log"
"github.com/influxdata/chronograf/uuid"
client "github.com/influxdata/usage-client/v1"
flags "github.com/jessevdk/go-flags"
"github.com/tylerb/graceful"
)
@ -30,15 +32,11 @@ func init() {
// Server for the chronograf API
type Server struct {
Host string `long:"host" description:"the IP to listen on" default:"0.0.0.0" env:"HOST"`
Port int `long:"port" description:"the port to listen on for insecure connections, defaults to a random value" default:"8888" env:"PORT"`
Host string `long:"host" description:"The IP to listen on" default:"0.0.0.0" env:"HOST"`
Port int `long:"port" description:"The port to listen on for insecure connections, defaults to a random value" default:"8888" env:"PORT"`
/* TODO: add in support for TLS
TLSHost string `long:"tls-host" description:"the IP to listen on for tls, when not specified it's the same as --host" env:"TLS_HOST"`
TLSPort int `long:"tls-port" description:"the port to listen on for secure connections, defaults to a random value" env:"TLS_PORT"`
TLSCertificate flags.Filename `long:"tls-certificate" description:"the certificate to use for secure connections" env:"TLS_CERTIFICATE"`
TLSCertificateKey flags.Filename `long:"tls-key" description:"the private key to use for secure conections" env:"TLS_PRIVATE_KEY"`
*/
Cert flags.Filename `long:"cert" description:"Path to PEM encoded public key certificate. " env:"TLS_CERTIFICATE"`
Key flags.Filename `long:"key" description:"Path to private key associated with given certificate. " env:"TLS_PRIVATE_KEY"`
Develop bool `short:"d" long:"develop" description:"Run server in develop mode."`
BoltPath string `short:"b" long:"bolt-path" description:"Full path to boltDB file (/var/lib/chronograf/chronograf-v1.db)" env:"BOLT_PATH" default:"chronograf-v1.db"`
@ -66,11 +64,47 @@ func (s *Server) useAuth() bool {
return s.TokenSecret != "" && s.GithubClientID != "" && s.GithubClientSecret != ""
}
func (s *Server) useTLS() bool {
return s.Cert != ""
}
// NewListener will an http or https listener depending useTLS()
func (s *Server) NewListener() (net.Listener, error) {
addr := net.JoinHostPort(s.Host, strconv.Itoa(s.Port))
if !s.useTLS() {
listener, err := net.Listen("tcp", addr)
if err != nil {
return nil, err
}
return listener, nil
}
// If no key specified, therefore, we assume it is in the cert
if s.Key == "" {
s.Key = s.Cert
}
cert, err := tls.LoadX509KeyPair(string(s.Cert), string(s.Key))
if err != nil {
return nil, err
}
listener, err := tls.Listen("tcp", addr, &tls.Config{
Certificates: []tls.Certificate{cert},
})
if err != nil {
return nil, err
}
return listener, nil
}
// Serve starts and runs the chronograf server
func (s *Server) Serve() error {
logger := clog.New(clog.ParseLevel(s.LogLevel))
service := openService(s.BoltPath, s.CannedPath, logger, s.useAuth())
basepath = s.Basepath
s.handler = NewMux(MuxOpts{
Develop: s.Develop,
TokenSecret: s.TokenSecret,
@ -81,16 +115,22 @@ func (s *Server) Serve() error {
UseAuth: s.useAuth(),
}, service)
// Add chronograf's version header to all requests
s.handler = Version(s.BuildInfo.Version, s.handler)
var err error
s.Listener, err = net.Listen("tcp", net.JoinHostPort(s.Host, strconv.Itoa(s.Port)))
if s.useTLS() {
// Add HSTS to instruct all browsers to change from http to https
s.handler = HSTS(s.handler)
}
listener, err := s.NewListener()
if err != nil {
logger.
WithField("component", "server").
Error(err)
return err
}
s.Listener = listener
httpServer := &graceful.Server{Server: new(http.Server)}
httpServer.SetKeepAlivesEnabled(true)
@ -100,10 +140,13 @@ func (s *Server) Serve() error {
if !s.ReportingDisabled {
go reportUsageStats(s.BuildInfo, logger)
}
scheme := "http"
if s.useTLS() {
scheme = "https"
}
logger.
WithField("component", "server").
Info("Serving chronograf at http://", s.Listener.Addr())
Info("Serving chronograf at ", scheme, "://", s.Listener.Addr())
if err := httpServer.Serve(s.Listener); err != nil {
logger.
@ -114,7 +157,7 @@ func (s *Server) Serve() error {
logger.
WithField("component", "server").
Info("Stopped serving chronograf at http://", s.Listener.Addr())
Info("Stopped serving chronograf at ", scheme, "://", s.Listener.Addr())
return nil
}