Skip to content

Commit

Permalink
Merge e83cdec into eb264fe
Browse files Browse the repository at this point in the history
  • Loading branch information
lukasmacko committed May 29, 2018
2 parents eb264fe + e83cdec commit 4044ddd
Show file tree
Hide file tree
Showing 4 changed files with 208 additions and 7 deletions.
80 changes: 80 additions & 0 deletions rpc/rest/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -47,3 +47,83 @@ $ curl -X GET http://localhost:9191/example
"Example": "This is an example"
}
```


## Security

REST plugin allows to optionally configure following security features:
- server certificate (HTTPS)
- Basic HTTP Authentication - username & password
- client certificates

All of them are disabled by default and can be enabled by config file:

```yaml
endpoint: 127.0.0.1:9292
server-cert-file: server.crt
server-key-file: server.key
client-cert-files:
- "ca.crt"
client-basic-auth:
- "user:pass"
- "foo:bar"
```

If `server-cert-file` and `server-key-file` are defined the server requires HTTPS instead
of HTTP for all its endpoints.

`client-cert-files` the list of the root certificate authorities that server uses to validate
client certificates. If the list is not empty only client who provide a valid certificate
is allowed to access the server.

`client-basic-auth` allows to define user password pairs that are allowed to access the
server. The config option defines a static list of allowed user. If the list is not empty default
staticAuthenticator is instantiated. Alternatively, you can implement custom authenticator and inject it
into the plugin (e.g.: if you want to read credentials from ETCD).


***Example***

In order to generated self-signed certificates you can use the following commands:

```bash
#generate key for "Our Certificate Authority"
openssl genrsa -out ca.key 2048

#generate certificate for CA
openssl req -new -nodes -x509 -key ca.key -out ca.crt -subj '/CN=CA'

#generate certificate for the server assume that server will be accessed by 127.0.0.1
openssl genrsa -out server.key 2048
openssl req -new -key server.key -out server.csr -subj '/CN=127.0.0.1'
openssl x509 -req -extensions client_server_ssl -extfile openssl_ext.conf -in server.csr -CA ca.crt -CAkey ca.key -CAcreateserial -out server.crt

#generate client certificate
openssl genrsa -out client.key 2048
openssl req -new -key client.key -out client.csr -subj '/CN=client'
openssl x509 -req -in client.csr -CA ca.crt -CAkey ca.key -CAcreateserial -out client.crt -days 360

```

Once the security features are enabled, the endpoint can be accessed by the following commands:

- **HTTPS**
where `ca.pem` is a certificate authority where server certificate should be validated (in case of self-signed certificates)
```
curl --cacert ca.crt https://127.0.0.1:9292/log/list
```

- **HTTPS + client cert** where `client.crt` is a valid client certificate.
```
curl --cacert ca.crt --cert client.crt --key client.key https://127.0.0.1:9292/log/list
```

- **HTTPS + basic auth** where `user:pass` is a valid username password pair.
```
curl --cacert ca.crt -u user:pass https://127.0.0.1:9292/log/list
```

- **HTTPS + client cert + basic auth**
```
curl --cacert ca.crt --cert client.crt --key client.key -u user:pass https://127.0.0.1:9292/log/list
```
21 changes: 21 additions & 0 deletions rpc/rest/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,22 @@ type Config struct {
// size of the request body.
// If zero, DefaultMaxHeaderBytes is used.
MaxHeaderBytes int

// ServerCertfile is path to the server certificate. If the certificate and corresponding
// key (see config item below) is defined server uses HTTPS instead of HTTP.
ServerCertfile string `json:"server-cert-file"`

// ServerKeyfile is path to the server key file.
ServerKeyfile string `json:"server-key-file"`

// ClientBasicAuth is a slice of credentials in form "username:password"
// used for basic HTTP authentication. If defined only authenticated users are allowed
// to access the server.
ClientBasicAuth []string `json:"client-basic-auth"`

// ClientCerts is a slice of the root certificate authorities
// that servers uses to verify a client certificate
ClientCerts []string `json:"client-cert-files"`
}

// GetPort parses suffix from endpoint & returns integer after last ":" (otherwise it returns 0)
Expand All @@ -114,6 +130,11 @@ func (cfg *Config) GetPort() int {
return 0
}

// UseHTTPS returns true if server certificate and key is defined.
func (cfg *Config) UseHTTPS() bool {
return cfg.ServerCertfile != "" && cfg.ServerKeyfile != ""
}

// DeclareHTTPPortFlag declares http port (with usage & default value) a flag for a particular plugin name
func DeclareHTTPPortFlag(pluginName core.PluginName, defaultPortOpts ...uint) {
var defaultPort string
Expand Down
34 changes: 31 additions & 3 deletions rpc/rest/listen_and_serve.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,10 @@
package rest

import (
"crypto/tls"
"crypto/x509"
"io"
"io/ioutil"
"net/http"
"time"
)
Expand All @@ -39,23 +42,48 @@ func FromExistingServer(listenAndServe ListenAndServe) *Plugin {

// ListenAndServeHTTP starts a http server.
func ListenAndServeHTTP(config Config, handler http.Handler) (httpServer io.Closer, err error) {

tlsCfg := &tls.Config{}

if len(config.ClientCerts) > 0 {
// require client certificate
caCertPool := x509.NewCertPool()

for _, c := range config.ClientCerts {
caCert, err := ioutil.ReadFile(c)
if err != nil {
return nil, err
}
caCertPool.AppendCertsFromPEM(caCert)
}

tlsCfg.ClientAuth = tls.RequireAndVerifyClientCert
tlsCfg.ClientCAs = caCertPool
}

server := &http.Server{
Addr: config.Endpoint,
ReadTimeout: config.ReadTimeout,
ReadHeaderTimeout: config.ReadHeaderTimeout,
WriteTimeout: config.WriteTimeout,
IdleTimeout: config.IdleTimeout,
MaxHeaderBytes: config.MaxHeaderBytes,
TLSConfig: tlsCfg,
}
server.Handler = handler

var errCh chan error
go func() {
if err := server.ListenAndServe(); err != nil {
errCh <- err
var err error
if config.UseHTTPS() {
// if server certificate and key is configured use HTTPS
err = server.ListenAndServeTLS(config.ServerCertfile, config.ServerKeyfile)
} else {
errCh <- nil
err = server.ListenAndServe()
}

errCh <- err

}()

select {
Expand Down
80 changes: 76 additions & 4 deletions rpc/rest/plugin_impl_rest.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,12 +18,14 @@ import (
"io"
"net/http"

"fmt"
"github.com/gorilla/mux"
"github.com/ligato/cn-infra/config"
"github.com/ligato/cn-infra/core"
"github.com/ligato/cn-infra/logging"
"github.com/ligato/cn-infra/utils/safeclose"
"github.com/unrolled/render"
"strings"
)

const (
Expand All @@ -35,6 +37,12 @@ const (
DefaultEndpoint = DefaultIP + ":" + DefaultHTTPPort
)

// BasicHTTPAuthenticator is a delegate that implements basic HTTP authentication
type BasicHTTPAuthenticator interface {
// Authenticate returns true if user is authenticated successfully, false otherwise.
Authenticate(user string, pass string) bool
}

// Plugin struct holds all plugin-related data.
type Plugin struct {
Deps
Expand All @@ -50,9 +58,14 @@ type Plugin struct {

// Deps lists the dependencies of the Rest plugin.
type Deps struct {
Log logging.PluginLogger //inject
PluginName core.PluginName //inject
config.PluginConfig //inject
Log logging.PluginLogger //inject
PluginName core.PluginName //inject
// Authenticator can be injected in a flavor inject method.
// If there is no authenticator injected and config contains
// user password, the default staticAuthenticator is instantiated.
// By default the authenticator is disabled.
Authenticator BasicHTTPAuthenticator //inject
config.PluginConfig //inject
}

// Init is the plugin entry point called by Agent Core
Expand All @@ -65,6 +78,15 @@ func (plugin *Plugin) Init() (err error) {
return err
}

// if there is no injected authenticator and there are credentials defined in the config file
// instantiate staticAuthenticator otherwise do not use basic Auth
if plugin.Authenticator == nil && len(plugin.Config.ClientBasicAuth) > 0 {
plugin.Authenticator, err = newStaticAuthenticator(plugin.Config.ClientBasicAuth)
if err != nil {
return err
}
}

plugin.mx = mux.NewRouter()
plugin.formatter = render.New(render.Options{
IndentJSON: true,
Expand All @@ -79,7 +101,11 @@ func (plugin *Plugin) RegisterHTTPHandler(path string,
methods ...string) *mux.Route {
plugin.Log.Debug("Register handler ", path)

if plugin.Authenticator != nil {
return plugin.mx.HandleFunc(path, auth(handler(plugin.formatter), plugin.Authenticator)).Methods(methods...)
}
return plugin.mx.HandleFunc(path, handler(plugin.formatter)).Methods(methods...)

}

// GetPort returns plugin configuration port
Expand All @@ -97,7 +123,12 @@ func (plugin *Plugin) AfterInit() (err error) {
if plugin.listenAndServe != nil {
plugin.server, err = plugin.listenAndServe(cfgCopy, plugin.mx)
} else {
plugin.Log.Info("Listening on http://", cfgCopy.Endpoint)
if cfgCopy.UseHTTPS() {
plugin.Log.Info("Listening on https://", cfgCopy.Endpoint)
} else {
plugin.Log.Info("Listening on http://", cfgCopy.Endpoint)
}

plugin.server, err = ListenAndServeHTTP(cfgCopy, plugin.mx)
}

Expand All @@ -117,3 +148,44 @@ func (plugin *Plugin) String() string {
}
return "HTTP"
}

func auth(fn http.HandlerFunc, auth BasicHTTPAuthenticator) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
user, pass, _ := r.BasicAuth()
if !auth.Authenticate(user, pass) {
w.Header().Set("WWW-Authenticate", "Provide valid username and password")
http.Error(w, "Unauthorized.", http.StatusUnauthorized)
return
}
fn(w, r)
}
}

// staticAuthenticator is default implementation of BasicHTTPAuthenticator
type staticAuthenticator struct {
credentials map[string]string
}

// newStaticAuthenticator creates new instance of static authenticator.
// Argument `users` is a slice of colon-separated username and password couples.
func newStaticAuthenticator(users []string) (*staticAuthenticator, error) {
sa := &staticAuthenticator{credentials: map[string]string{}}
for _, u := range users {
fields := strings.Split(u, ":")
if len(fields) != 2 {
return nil, fmt.Errorf("invalid format of basic auth entry '%v' expected 'user:pass'", u)
}
sa.credentials[fields[0]] = fields[1]
}
return sa, nil
}

// Authenticate looks up the given user name and password in the internal map.
// If match is found returns true, false otherwise.
func (sa *staticAuthenticator) Authenticate(user string, pass string) bool {
password, found := sa.credentials[user]
if !found {
return false
}
return pass == password
}

0 comments on commit 4044ddd

Please sign in to comment.