Skip to content
Snippets Groups Projects
Commit fd58db80 authored by Rohith's avatar Rohith
Browse files

Self Signed Certificate

- adding the options for the proxy to generate and rotate it's own self-signed ceritificates
parent c4d677ad
Branches
No related tags found
No related merge requests found
......@@ -27,7 +27,7 @@
Keycloak-proxy is a proxy service which at the risk of stating the obvious integrates with the [Keycloak](https://github.com/keycloak/keycloak) authentication service. Although technically the service has no dependency on Keycloak itself and would quite happily work with any OpenID provider. The service supports both access tokens in browser cookie or bearer tokens.
```shell
$ bin/keycloak-proxy help
$ bin/keycloak-proxy --help
NAME:
keycloak-proxy - is a proxy using the keycloak service for auth and authorization
......@@ -35,7 +35,7 @@ USAGE:
keycloak-proxy [options]
VERSION:
v2.2.0 (git+sha: 72a3646-dirty, built: 25-05-2018)
v2.2.2 (git+sha: c4d677a-dirty, built: 12-07-2018)
AUTHOR:
Rohith <gambol99@gmail.com>
......@@ -55,6 +55,7 @@ GLOBAL OPTIONS:
--skip-openid-provider-tls-verify skip the verification of any TLS communication with the openid provider (default: false)
--openid-provider-proxy value proxy for communication with the openid provider
--openid-provider-timeout value timeout for openid configuration on .well-known/openid-configuration (default: 30s)
--base-uri value common prefix for all URIs [$PROXY_BASE_URI]
--oauth-uri value the uri for proxy oauth endpoints (default: "/oauth") [$PROXY_OAUTH_URI]
--scopes value list of scopes requested when authenticating the user
--upstream-url value url for the upstream endpoint you wish to proxy [$PROXY_UPSTREAM_URL]
......@@ -62,15 +63,21 @@ GLOBAL OPTIONS:
--resources value list of resources 'uri=/admin*|methods=GET,PUT|roles=role1,role2'
--headers value custom headers to the upstream request, key=value
--preserve-host preserve the host header of the proxied request in the upstream request (default: false)
--request-id-header value the http header name for request id (default: "X-Request-ID") [$PROXY_REQUEST_ID_HEADER]
--response-headers value custom headers to added to the http response key=value
--enable-self-signed-tls create self signed certificates for the proxy (default: false) [$PROXY_ENABLE_SELF_SIGNED_TLS]
--self-signed-tls-hostnames value a list of hostnames to place on the self-signed certificate
--self-signed-tls-expiration value the expiration of the certificate before rotation (default: 3h0m0s)
--enable-request-id indicates we should add a request id if none found (default: false) [$PROXY_ENABLE_REQUEST_ID]
--enable-logout-redirect indicates we should redirect to the identity provider for logging out (default: false)
--enable-default-deny enables a default denial on all requests, you have to explicitly say what is permitted (recommended) (default: false)
--enable-default-deny enables a default denial on all requests, you have to explicitly say what is permitted (recommended) (default: true)
--enable-encrypted-token enable encryption for the access tokens (default: false)
--enable-logging enable http logging of the requests (default: false)
--enable-json-logging switch on json logging rather than text (default: false)
--enable-forwarding enables the forwarding proxy mode, signing outbound request (default: false)
--enable-security-filter enables the security filter handler (default: false) [$PROXY_ENABLE_SECURITY_FILTER]
--enable-refresh-tokens enables the handling of the refresh tokens (default: false) [$PROXY_ENABLE_REFRESH_TOKEN]
--enable-session-cookies access and refresh tokens are session only i.e. removed browser close (default: false)
--enable-session-cookies access and refresh tokens are session only i.e. removed browser close (default: true)
--enable-login-handler enables the handling of the refresh tokens (default: false) [$PROXY_ENABLE_LOGIN_HANDLER]
--enable-token-header enables the token authentication header X-Auth-Token to upstream (default: true)
--enable-authorization-header adds the authorization header to the proxy request (default: true) [$PROXY_ENABLE_AUTHORIZATION_HEADER]
......@@ -107,6 +114,7 @@ GLOBAL OPTIONS:
--hostnames value list of hostnames the service will respond to
--store-url value url for the storage subsystem, e.g redis://127.0.0.1:6379, file:///etc/tokens.file
--encryption-key value encryption key used to encryption the session state [$PROXY_ENCRYPTION_KEY]
--invalid-auth-redirects-with-303 use HTTP 303 redirects instead of 307 for invalid auth tokens (default: false)
--no-redirects do not have back redirects when no authentication is present, 401 them (default: false)
--skip-token-verification TESTING ONLY; bypass token verification, only expiration and roles enforced (default: false)
--upstream-keepalives enables or disables the keepalive connections for upstream endpoint (default: true)
......@@ -138,8 +146,8 @@ GLOBAL OPTIONS:
Assuming you have make + go, simply run make (or 'make static' for static linking). You can also build via docker container: make docker-build
#### **Docker image**
Docker image is available at [https://quay.io/repository/gambol99/keycloak-proxy](https://quay.io/repository/gambol99/keycloak-proxy)
Docker image is available at [https://quay.io/repository/gambol99/keycloak-proxy](https://quay.io/repository/gambol99/keycloak-proxy)
#### **Configuration**
......@@ -269,6 +277,23 @@ By default all requests will be proxyed on to the upstream, if you wish to ensur
Note the HTTP routing rules following the guidelines from [chi](https://github.com/go-chi/chi#router-design). Its also worth nothing the ordering of the resource do not matter, the router will handle that for you.
#### **Resources**
The resources defined either on the command line as `--resources` or via a configuration file defines a collection of enrtypoints and the requirement for access.
```YAML
resources:
- uri: /admin/*
roles:
- admin
- superuser
# will work with either 'admin' or 'superuser' the default is false and requires both roles present
require-any-role: true
- uri: /public/*
# indicates we permit access regardless
white-listed: true
```
#### **Google OAuth**
Although the role extensions do require a Keycloak IDP or at the very least a IDP that produces a token which contains roles, there's nothing stopping you from using it against any OpenID providers, such as Google. Go to the Google Developers Console/Google Cloud Console and create a new OAuth 2.0 client ID *(via "API Manager-> Credentials)*. Once you've created the OAuth 2.0 client ID, take the client ID, secret and make sure you've added the callback url to the application scope *(using the default this would be http://127.0.0.1:3000/oauth/callback)*
......@@ -430,6 +455,10 @@ X-Auth-Given-Name: Rohith
X-Auth-Name: Rohith Jayawardene
```
#### **Self Signed Certificate**
The proxy can be instructed to generate it's on self-signed certificate which are rotated on a user-defined expiration. Add the `--enable-self-signed-tls=true` option to the config or command line and if required you can configure the hostnames and expiration via the `--self-signed-tls-hostnames` and `--self-signed-tls-expiration`
#### **Encryption Key**
In order to remain stateless and not have to rely on a central cache to persist the 'refresh_tokens', the refresh token is encrypted and added as a cookie using *crypto/aes*. Naturally the key must be the same if your running behind a load balancer etc. The key length should either 16 or 32 bytes depending or whether you want AES-128 or AES-256.
......
......
......@@ -19,6 +19,7 @@ import (
"errors"
"fmt"
"net/url"
"os"
"regexp"
"strings"
"time"
......@@ -26,6 +27,12 @@ import (
// newDefaultConfig returns a initialized config
func newDefaultConfig() *Config {
var hostnames []string
if name, err := os.Hostname(); err == nil {
hostnames = append(hostnames, name)
}
hostnames = append(hostnames, []string{"localhost", "127.0.0.1"}...)
return &Config{
AccessTokenDuration: time.Duration(720) * time.Hour,
CookieAccessName: "kc-access",
......@@ -35,6 +42,8 @@ func newDefaultConfig() *Config {
EnableDefaultDeny: true,
EnableSessionCookies: true,
EnableTokenHeader: true,
SelfSignedTLSHostnames: hostnames,
SelfSignedTLSExpiration: 3 * time.Hour,
Headers: make(map[string]string),
LetsEncryptCacheDir: "./cache/",
MatchClaims: make(map[string]string),
......
......
......@@ -35,7 +35,7 @@ var (
const (
prog = "keycloak-proxy"
author = "Rohith"
author = "Rohith Jayawardene"
email = "gambol99@gmail.com"
description = "is a proxy using the keycloak service for auth and authorization"
......@@ -183,6 +183,13 @@ type Config struct {
// ResponseHeader is a map of response headers to add to the response
ResponseHeaders map[string]string `json:"response-headers" yaml:"response-headers" usage:"custom headers to added to the http response key=value"`
// EnableSelfSignedTLS indicates we should create a self-signed ceritificate for the service
EnabledSelfSignedTLS bool `json:"enable-self-signed-tls" yaml:"enable-self-signed-tls" usage:"create self signed certificates for the proxy" env:"ENABLE_SELF_SIGNED_TLS"`
// SelfSignedTLSHostnames is the list of hostnames to place on the certificate
SelfSignedTLSHostnames []string `json:"self-signed-tls-hostnames" yaml:"self-signed-tls-hostnames" usage:"a list of hostnames to place on the self-signed certificate"`
// SelfSignedTLSExpiration is the expiration time of the tls certificate before rotation occurs
SelfSignedTLSExpiration time.Duration `json:"self-signed-tls-expiration" yaml:"self-signed-tls-expiration" usage:"the expiration of the certificate before rotation"`
// EnableRequestID indicates the proxy should add request id if none if found
EnableRequestID bool `json:"enable-request-id" yaml:"enable-request-id" usage:"indicates we should add a request id if none found" env:"ENABLE_REQUEST_ID"`
// EnableLogoutRedirect indicates we should redirect to the identity provider for logging out
......
......
......@@ -44,7 +44,7 @@ func newCertificateRotator(cert, key string, log *zap.Logger) (*certificationRot
if err != nil {
return nil, err
}
// step: are we watching the files for changes?
// @step: are we watching the files for changes?
return &certificationRotation{
certificate: certificate,
certificateFile: cert,
......
......
/*
Copyright 2018 All rights reserved.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package main
import (
"context"
"crypto/rand"
"crypto/rsa"
"crypto/tls"
"errors"
"sync"
"time"
"go.uber.org/zap"
)
type selfSignedCertificate struct {
sync.RWMutex
// certificate holds the current issuing certificate
certificate tls.Certificate
// expiration is the certificate expiration
expiration time.Duration
// hostnames is the list of host names on the certificate
hostnames []string
// privateKey is the rsa private key
privateKey *rsa.PrivateKey
// the logger for this service
log *zap.Logger
// stopCh is a channel to close off the rotation
cancel context.CancelFunc
}
// newSelfSignedCertificate creates and returns a self signed certificate manager
func newSelfSignedCertificate(hostnames []string, expiry time.Duration, log *zap.Logger) (*selfSignedCertificate, error) {
if len(hostnames) <= 0 {
return nil, errors.New("no hostnames specified")
}
if expiry < 5*time.Minute {
return nil, errors.New("expiration must be greater then 5 minutes")
}
// @step: generate a certificate pair
log.Info("generating a private key for self-signed certificate", zap.String("common_name", hostnames[0]))
key, err := rsa.GenerateKey(rand.Reader, 2048)
if err != nil {
return nil, err
}
// @step: create an initial certificate
certificate, err := createCertificate(key, hostnames, expiry)
if err != nil {
return nil, err
}
// @step: create a context to run under
ctx, cancel := context.WithCancel(context.Background())
svc := &selfSignedCertificate{
certificate: certificate,
expiration: expiry,
hostnames: hostnames,
log: log,
privateKey: key,
cancel: cancel,
}
if err := svc.rotate(ctx); err != nil {
return nil, err
}
return svc, nil
}
// rotate is responsible for rotation the certificate
func (c *selfSignedCertificate) rotate(ctx context.Context) error {
go func() {
c.log.Info("starting the self-signed certificate rotation",
zap.Duration("expiration", c.expiration))
for {
expires := time.Now().Add(c.expiration).Add(-5 * time.Minute)
ticker := expires.Sub(time.Now())
select {
case <-ctx.Done():
return
case <-time.After(ticker):
}
c.log.Info("going to sleep until required for rotation", zap.Time("expires", expires), zap.Duration("duration", expires.Sub(time.Now())))
// @step: got to sleep until we need to rotate
time.Sleep(expires.Sub(time.Now()))
// @step: create a new certificate for us
cert, _ := createCertificate(c.privateKey, c.hostnames, c.expiration)
c.log.Info("updating the certificate for server")
// @step: update the current certificate
c.updateCertificate(cert)
}
}()
return nil
}
// close is used to shutdown resources
func (c *selfSignedCertificate) close() {
c.cancel()
}
// updateCertificate is responsible for update the certificate
func (c *selfSignedCertificate) updateCertificate(cert tls.Certificate) {
c.Lock()
defer c.Unlock()
c.certificate = cert
}
// GetCertificate is responsible for retrieving
func (c *selfSignedCertificate) GetCertificate(hello *tls.ClientHelloInfo) (*tls.Certificate, error) {
c.RLock()
defer c.RUnlock()
return &c.certificate, nil
}
/*
Copyright 2018 All rights reserved.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package main
import (
"crypto/tls"
"testing"
"time"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"go.uber.org/zap"
)
func TestNewSelfSignedCertificate(t *testing.T) {
c, err := newSelfSignedCertificate([]string{"localhost"}, 5*time.Minute, zap.NewNop())
assert.NoError(t, err)
assert.NotNil(t, c)
c.close()
}
func TestSelfSignedNoHostnames(t *testing.T) {
c, err := newSelfSignedCertificate([]string{}, 5*time.Minute, zap.NewNop())
assert.Error(t, err)
assert.Nil(t, c)
}
func TestSelfSignedExpirationBad(t *testing.T) {
c, err := newSelfSignedCertificate([]string{"localhost"}, 1*time.Minute, zap.NewNop())
assert.Error(t, err)
assert.Nil(t, c)
}
func TestSelfSignedGetCertificate(t *testing.T) {
c, err := newSelfSignedCertificate([]string{"localhost"}, 5*time.Minute, zap.NewNop())
require.NoError(t, err)
require.NotNil(t, c)
defer c.close()
cert, err := c.GetCertificate(&tls.ClientHelloInfo{})
assert.NoError(t, err)
assert.NotNil(t, cert)
}
......@@ -65,7 +65,6 @@ type oauthProxy struct {
func init() {
time.LoadLocation("UTC") // ensure all time is in UTC
runtime.GOMAXPROCS(runtime.NumCPU()) // set the core
// @step: register the instrumentation
prometheus.MustRegister(certificateRotationMetric)
prometheus.MustRegister(latencyMetric)
prometheus.MustRegister(oauthLatencyMetric)
......@@ -352,16 +351,18 @@ func (r *oauthProxy) createForwardingProxy() error {
// Run starts the proxy service
func (r *oauthProxy) Run() error {
listener, err := r.createHTTPListener(listenerConfig{
listen: r.config.Listen,
certificate: r.config.TLSCertificate,
privateKey: r.config.TLSPrivateKey,
ca: r.config.TLSCaCertificate,
certificate: r.config.TLSCertificate,
clientCert: r.config.TLSClientCertificate,
proxyProtocol: r.config.EnableProxyProtocol,
useLetsEncrypt: r.config.UseLetsEncrypt,
letsEncryptCacheDir: r.config.LetsEncryptCacheDir,
hostnames: r.config.Hostnames,
letsEncryptCacheDir: r.config.LetsEncryptCacheDir,
listen: r.config.Listen,
privateKey: r.config.TLSPrivateKey,
proxyProtocol: r.config.EnableProxyProtocol,
redirectionURL: r.config.RedirectionURL,
useFileTLS: r.config.TLSPrivateKey != "" && r.config.TLSCertificate != "",
useLetsEncryptTLS: r.config.UseLetsEncrypt,
useSelfSignedTLS: r.config.EnabledSelfSignedTLS,
})
if err != nil {
......@@ -416,16 +417,18 @@ func (r *oauthProxy) Run() error {
// listenerConfig encapsulate listener options
type listenerConfig struct {
listen string // the interface to bind the listener to
certificate string // the path to the certificate if any
privateKey string // the path to the private key if any
ca string // the path to a certificate authority
certificate string // the path to the certificate if any
clientCert string // the path to a client certificate to use for mutual tls
proxyProtocol bool // whether to enable proxy protocol on the listen
hostnames []string // list of hostnames the service will respond to
redirectionURL string // url to redirect to
useLetsEncrypt bool // whether to use lets encrypt for retrieving ssl certificates
letsEncryptCacheDir string // the path to cache letsencrypt certificates
listen string // the interface to bind the listener to
privateKey string // the path to the private key if any
proxyProtocol bool // whether to enable proxy protocol on the listen
redirectionURL string // url to redirect to
useFileTLS bool // indicates we are using certificates from files
useLetsEncryptTLS bool // indicates we are using letsencrypt
useSelfSignedTLS bool // indicates we are using the self-signed tls
}
// ErrHostNotConfigured indicates the hostname was not configured
......@@ -460,13 +463,15 @@ func (r *oauthProxy) createHTTPListener(config listenerConfig) (net.Listener, er
listener = &proxyproto.Listener{Listener: listener}
}
// does the socket require TLS?
if (config.certificate != "" && config.privateKey != "") || config.useLetsEncrypt {
// @check if the socket requires TLS
if config.useSelfSignedTLS || config.useLetsEncryptTLS || config.useFileTLS {
getCertificate := func(hello *tls.ClientHelloInfo) (*tls.Certificate, error) {
return nil, errors.New("Not configured")
}
if config.useLetsEncrypt {
if config.useLetsEncryptTLS {
r.log.Info("enabling letsencrypt tls support")
m := autocert.Manager{
Prompt: autocert.AcceptTOS,
Cache: autocert.DirCache(config.letsEncryptCacheDir),
......@@ -494,9 +499,21 @@ func (r *oauthProxy) createHTTPListener(config listenerConfig) (net.Listener, er
}
getCertificate = m.GetCertificate
} else {
}
if config.useSelfSignedTLS {
r.log.Info("enabling self-signed tls support", zap.Duration("expiration", r.config.SelfSignedTLSExpiration))
rotate, err := newSelfSignedCertificate(r.config.SelfSignedTLSHostnames, r.config.SelfSignedTLSExpiration, r.log)
if err != nil {
return nil, err
}
getCertificate = rotate.GetCertificate
}
if config.useFileTLS {
r.log.Info("tls support enabled", zap.String("certificate", config.certificate), zap.String("private_key", config.privateKey))
// creating a certificate rotation
rotate, err := newCertificateRotator(config.certificate, config.privateKey, r.log)
if err != nil {
return nil, err
......@@ -510,13 +527,13 @@ func (r *oauthProxy) createHTTPListener(config listenerConfig) (net.Listener, er
}
tlsConfig := &tls.Config{
PreferServerCipherSuites: true,
GetCertificate: getCertificate,
PreferServerCipherSuites: true,
}
listener = tls.NewListener(listener, tlsConfig)
// are we doing mutual tls?
// @check if we doing mutual tls
if config.clientCert != "" {
caCert, err := ioutil.ReadFile(config.clientCert)
if err != nil {
......
......
......@@ -19,15 +19,19 @@ import (
"crypto/aes"
"crypto/cipher"
"crypto/rand"
"crypto/rsa"
sha "crypto/sha256"
"crypto/tls"
"crypto/x509"
"crypto/x509/pkix"
"encoding/base64"
"encoding/json"
"encoding/pem"
"errors"
"fmt"
"io"
"io/ioutil"
"math/big"
"net"
"net/http"
"net/url"
......@@ -62,6 +66,52 @@ var (
symbolsFilter = regexp.MustCompilePOSIX("[_$><\\[\\].,\\+-/'%^&*()!\\\\]+")
)
// createCertificate is responsible for creating a certificate
func createCertificate(key *rsa.PrivateKey, hostnames []string, expire time.Duration) (tls.Certificate, error) {
// @step: create a serial for the certificate
serial, err := rand.Int(rand.Reader, new(big.Int).Lsh(big.NewInt(1), 128))
if err != nil {
return tls.Certificate{}, err
}
template := x509.Certificate{
BasicConstraintsValid: true,
ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth},
IsCA: false,
KeyUsage: x509.KeyUsageKeyEncipherment | x509.KeyUsageDigitalSignature,
NotAfter: time.Now().Add(expire),
NotBefore: time.Now().Add(-30 * time.Second),
PublicKeyAlgorithm: x509.ECDSA,
SerialNumber: serial,
SignatureAlgorithm: x509.SHA512WithRSA,
Subject: pkix.Name{
CommonName: hostnames[0],
Organization: []string{"Keycloak Proxy"},
},
}
// @step: add the hostnames to the certificate template
if len(hostnames) > 1 {
for _, x := range hostnames[1:] {
if ip := net.ParseIP(x); ip != nil {
template.IPAddresses = append(template.IPAddresses, ip)
} else {
template.DNSNames = append(template.DNSNames, x)
}
}
}
// @step: create the certificate
cert, err := x509.CreateCertificate(rand.Reader, &template, &template, &key.PublicKey, key)
if err != nil {
return tls.Certificate{}, err
}
certPEM := pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: cert})
keyPEM := pem.EncodeToMemory(&pem.Block{Type: "RSA PRIVATE KEY", Bytes: x509.MarshalPKCS1PrivateKey(key)})
return tls.X509KeyPair(certPEM, keyPEM)
}
// getRequestHostURL returns the hostname from the request
func getRequestHostURL(r *http.Request) string {
hostname := r.Host
......
......
......@@ -17,6 +17,7 @@ package main
import (
"bytes"
"crypto/tls"
"fmt"
"io/ioutil"
"net/http"
......@@ -65,6 +66,51 @@ func TestDecodeKeyPairs(t *testing.T) {
}
}
func TestGetRequestHostURL(t *testing.T) {
cs := []struct {
Expected string
HostHeader string
Hostname string
TLS *tls.ConnectionState
}{
{
Expected: "http://www.test.com",
Hostname: "www.test.com",
},
{
Expected: "http://",
},
{
Expected: "http://www.override.com",
HostHeader: "www.override.com",
Hostname: "www.test.com",
},
{
Expected: "https://www.test.com",
Hostname: "www.test.com",
TLS: &tls.ConnectionState{},
},
{
Expected: "https://www.override.com",
HostHeader: "www.override.com",
Hostname: "www.test.com",
TLS: &tls.ConnectionState{},
},
}
for i, c := range cs {
request := &http.Request{
Method: http.MethodGet,
Host: c.Hostname,
TLS: c.TLS,
}
if c.HostHeader != "" {
request.Header = make(http.Header, 0)
request.Header.Set("X-Forwarded-Host", c.HostHeader)
}
assert.Equal(t, c.Expected, getRequestHostURL(request), "case %d, expected: %s, got: %s", i, c.Expected, getRequestHostURL(request))
}
}
func BenchmarkUUID(b *testing.B) {
for n := 0; n < b.N; n++ {
s := uuid.NewV1()
......
......
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Please to comment