Skip to content

Commit

Permalink
Self Signed Certificate
Browse files Browse the repository at this point in the history
- adding the options for the proxy to generate and rotate it's own self-signed ceritificates
  • Loading branch information
gambol99 committed Jul 12, 2018
1 parent c4d677a commit fd58db8
Show file tree
Hide file tree
Showing 9 changed files with 381 additions and 27 deletions.
39 changes: 34 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,15 +27,15 @@
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

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 <[email protected]>
Expand All @@ -55,22 +55,29 @@ 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]
--upstream-ca value the path to a file container a CA certificate to validate the upstream tls endpoint
--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]
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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**
Expand Down Expand Up @@ -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)*
Expand Down Expand Up @@ -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.
Expand Down
9 changes: 9 additions & 0 deletions config.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,13 +19,20 @@ import (
"errors"
"fmt"
"net/url"
"os"
"regexp"
"strings"
"time"
)

// 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",
Expand All @@ -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),
Expand Down
9 changes: 8 additions & 1 deletion doc.go
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ var (

const (
prog = "keycloak-proxy"
author = "Rohith"
author = "Rohith Jayawardene"
email = "[email protected]"
description = "is a proxy using the keycloak service for auth and authorization"

Expand Down Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion rotation.go
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
140 changes: 140 additions & 0 deletions self_signed.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,140 @@
/*
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
}
56 changes: 56 additions & 0 deletions self_signed_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
/*
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)
}
Loading

0 comments on commit fd58db8

Please sign in to comment.