forked from louketo/louketo-proxy
-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- adding the options for the proxy to generate and rotate it's own self-signed ceritificates
- Loading branch information
Showing
9 changed files
with
381 additions
and
27 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -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]> | ||
|
@@ -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] | ||
|
@@ -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. | ||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -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" | ||
|
||
|
@@ -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 | ||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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) | ||
} |
Oops, something went wrong.