Skip to content

Commit

Permalink
Add JWT policy support
Browse files Browse the repository at this point in the history
  • Loading branch information
Dean-Coakley authored Sep 22, 2020
1 parent 8ffdeb3 commit 4cc4402
Show file tree
Hide file tree
Showing 23 changed files with 1,153 additions and 55 deletions.
10 changes: 10 additions & 0 deletions deployments/common/policy-definition.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,16 @@ spec:
type: array
items:
type: string
jwt:
description: JWTAuth holds JWT authentication configuration.
type: object
properties:
realm:
type: string
secret:
type: string
token:
type: string
rateLimit:
description: RateLimit defines a rate limit policy.
type: object
Expand Down
10 changes: 10 additions & 0 deletions deployments/helm-chart/crds/policy.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,16 @@ spec:
type: array
items:
type: string
jwt:
description: JWTAuth holds JWT authentication configuration.
type: object
properties:
realm:
type: string
secret:
type: string
token:
type: string
rateLimit:
description: RateLimit defines a rate limit policy.
type: object
Expand Down
55 changes: 55 additions & 0 deletions docs-web/configuration/policy-resource.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@ This document is the reference documentation for the Policy resource. An example
- [AccessControl Merging Behavior](#accesscontrol-merging-behavior)
- [RateLimit](#ratelimit)
- [RateLimit Merging Behavior](#ratelimit-merging-behavior)
- [JWT](#jwt)
- [JWT Merging Behavior](#jwt-merging-behavior)
- [Using Policy](#using-policy)
- [Validation](#validation)
- [Structural Validation](#structural-validation)
Expand Down Expand Up @@ -57,6 +59,10 @@ spec:
- The rate limit policy controls the rate of processing requests per a defined key.
- `rateLimit <#ratelimit>`_
- No*
* - ``JWT``
- The JWT policy configures NGINX Plus to authenticate client requests using JSON Web Tokens.
- `jwt <#jwt>`_
- No*
```
\* A policy must include exactly one policy.
Expand Down Expand Up @@ -112,6 +118,7 @@ When you reference more than one access control policy, the Ingress Controller w

Referencing both allow and deny policies, as shown in the example below, is not supported. If both allow and deny lists are referenced, the Ingress Controller uses just the allow list policies.
```yaml
policies:
- name: deny-policy
- name: allow-policy-one
- name: allow-policy-two
Expand Down Expand Up @@ -189,6 +196,54 @@ policies:

When you reference more than one rate limit policy, the Ingress Controller will configure NGINX to use all referenced rate limits. When you define multiple policies, each additional policy inherits the `dryRun`, `logLevel`, and `rejectCode` parameters from the first policy referenced (`rate-limit-policy-one`, in the example above).

### JWT

> Note: This feature is only available in NGINX Plus.

The JWT policy configures NGINX Plus to authenticate client requests using JSON Web Tokens.

For example, the following policy will reject all requests that do not include a valid JWT in the HTTP header `token`:
```yaml
jwt:
secret: jwk-secret
realm: "My API"
token: $http_token
```

> Note: The feature is implemented using the NGINX Plus [ngx_http_auth_jwt_module](https://nginx.org/en/docs/http/ngx_http_auth_jwt_module.html).

```eval_rst
.. list-table::
:header-rows: 1
* - Field
- Description
- Type
- Required
* - ``secret``
- The name of the Kubernetes secret that stores the JWK. It must be in the same namespace as the Policy resource. The JWK must be stored in the secret under the key ``jwk``, otherwise the secret will be rejected as invalid.
- ``string``
- Yes
* - ``realm``
- The realm of the JWT.
- ``string``
- Yes
* - ``token``
- The token specifies a variable that contains the JSON Web Token. By default the JWT is passed in the ``Authorization`` header as a Bearer Token. JWT may be also passed as a cookie or a part of a query string, for example: ``$cookie_auth_token``. Accepted variables are ``$http_``, ``$arg_``, ``$cookie_``.
- ``string``
- No
```

#### JWT Merging Behavior

A VirtualServer/VirtualServerRoute can reference multiple JWT policies. However, only one can be applied. Every subsequent reference will be ignored. For example, here we reference two policies:
```yaml
policies:
- name: jwt-policy-one
- name: jwt-policy-two
```
In this example the Ingress Controller will use the configuration from the first policy reference `jwt-policy-one`, and ignores `jwt-policy-two`.

## Using Policy

You can use the usual `kubectl` commands to work with Policy resources, just as with built-in Kubernetes resources.
Expand Down
69 changes: 69 additions & 0 deletions examples-of-custom-resources/jwt/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
# JWT

In this example, we deploy a web application, configure load balancing for it via a VirtualServer, and apply a JWT policy.

## Prerequisites

1. Follow the [installation](https://docs.nginx.com/nginx-ingress-controller/installation/installation-with-manifests/) instructions to deploy the Ingress Controller.
1. Save the public IP address of the Ingress Controller into a shell variable:
```
$ IC_IP=XXX.YYY.ZZZ.III
```
1. Save the HTTP port of the Ingress Controller into a shell variable:
```
$ IC_HTTP_PORT=<port number>
```

## Step 1 - Deploy a Web Application

Create the application deployment and service:
```
$ kubectl apply -f webapp.yaml
```

## Step 2 - Deploy the JWK Secret

Create a secret with the name `jwk-secret` that will be used for JWT validation:
```
$ kubectl apply -f jwk-secret.yaml
```

## Step 3 - Deploy the JWT Policy

Create a policy with the name `jwt-policy` that references the secret from the previous step and only permits requests to our web application that contain a valid JWT:
```
$ kubectl apply -f jwt.yaml
```

## Step 3 - Configure Load Balancing

Create a VirtualServer resource for the web application:
```
$ kubectl apply -f virtual-server.yaml
```

Note that the VirtualServer references the policy `jwt-policy` created in Step 3.

## Step 4 - Test the Configuration

If you attempt to access the application without providing a valid JWT, NGINX will reject your requests for that VirtualServer:
```
$ curl --resolve webapp.example.com:$IC_HTTP_PORT:$IC_IP http://webapp.example.com:$IC_HTTP_PORT/
<html>
<head><title>401 Authorization Required</title></head>
<body>
<center><h1>401 Authorization Required</h1></center>
<hr><center>nginx/1.19.1</center>
</body>
</html>
```

If you provide a valid JWT, your request will succeed:
```
$ curl --resolve webapp.example.com:$IC_HTTP_PORT:$IC_IP http://webapp.example.com:$IC_HTTP_PORT/ -H "token: `cat token.jwt`"
Server address: 172.17.0.3:8080
Server name: webapp-7c6d448df9-lcrx6
Date: 10/Sep/2020:18:20:03 +0000
URI: /
Request ID: db2c07ce640755ccbe9f666d16f85620
```
6 changes: 6 additions & 0 deletions examples-of-custom-resources/jwt/jwk-secret.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
kind: Secret
metadata:
name: jwk-secret
apiVersion: v1
data:
jwk: eyJrZXlzIjoKICAgIFt7CiAgICAgICAgImsiOiJabUZ1ZEdGemRHbGphbmQwIiwKICAgICAgICAia3R5Ijoib2N0IiwKICAgICAgICAia2lkIjoiMDAwMSIKICAgIH1dCn0K
9 changes: 9 additions & 0 deletions examples-of-custom-resources/jwt/jwt.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
apiVersion: k8s.nginx.org/v1alpha1
kind: Policy
metadata:
name: jwt-policy
spec:
jwt:
realm: MyProductAPI
secret: jwk-secret
token: $http_token
1 change: 1 addition & 0 deletions examples-of-custom-resources/jwt/token.jwt
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiIsImtpZCI6IjAwMDEifQ.eyJuYW1lIjoiUXVvdGF0aW9uIFN5c3RlbSIsInN1YiI6InF1b3RlcyIsImlzcyI6Ik15IEFQSSBHYXRld2F5In0.ggVOHYnVFB8GVPE-VOIo3jD71gTkLffAY0hQOGXPL2I
16 changes: 16 additions & 0 deletions examples-of-custom-resources/jwt/virtual-server.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
apiVersion: k8s.nginx.org/v1
kind: VirtualServer
metadata:
name: webapp
spec:
host: webapp.example.com
policies:
- name: jwt-policy
upstreams:
- name: webapp
service: webapp-svc
port: 80
routes:
- path: /
action:
pass: webapp
32 changes: 32 additions & 0 deletions examples-of-custom-resources/jwt/webapp.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
apiVersion: apps/v1
kind: Deployment
metadata:
name: webapp
spec:
replicas: 1
selector:
matchLabels:
app: webapp
template:
metadata:
labels:
app: webapp
spec:
containers:
- name: webapp
image: nginxdemos/nginx-hello:plain-text
ports:
- containerPort: 8080
---
apiVersion: v1
kind: Service
metadata:
name: webapp-svc
spec:
ports:
- port: 80
targetPort: 8080
protocol: TCP
name: http
selector:
app: webapp
40 changes: 38 additions & 2 deletions internal/configs/configurator.go
Original file line number Diff line number Diff line change
Expand Up @@ -406,8 +406,11 @@ func (cnf *Configurator) addOrUpdateVirtualServer(virtualServerEx *VirtualServer
if virtualServerEx.TLSSecret != nil {
tlsPemFileName = cnf.addOrUpdateTLSSecret(virtualServerEx.TLSSecret)
}

jwtKeys := cnf.addOrUpdateJWKSecretsForVirtualServer(virtualServerEx.JWTKeys)

vsc := newVirtualServerConfigurator(cnf.cfgParams, cnf.isPlus, cnf.IsResolverConfigured(), cnf.staticCfgParams)
vsCfg, warnings := vsc.GenerateVirtualServerConfig(virtualServerEx, tlsPemFileName)
vsCfg, warnings := vsc.GenerateVirtualServerConfig(virtualServerEx, tlsPemFileName, jwtKeys)
name := getFileNameForVirtualServer(virtualServerEx.VirtualServer)
content, err := cnf.templateExecutorV2.ExecuteVirtualServerTemplate(&vsCfg)
if err != nil {
Expand Down Expand Up @@ -580,8 +583,41 @@ func (cnf *Configurator) addOrUpdateJWKSecret(secret *api_v1.Secret) string {
return cnf.nginxManager.CreateSecret(name, data, nginx.JWKSecretFileMode)
}

func (cnf *Configurator) AddOrUpdateJWKSecret(secret *api_v1.Secret) {
// AddOrUpdateJWKSecret adds a JWK secret to the filesystem or updates it if it already exists.
func (cnf *Configurator) AddOrUpdateJWKSecret(secret *api_v1.Secret, virtualServerExes []*VirtualServerEx) error {
cnf.addOrUpdateJWKSecret(secret)

if len(virtualServerExes) > 0 {
for _, vsEx := range virtualServerExes {
// It is safe to ignore warnings here as no new warnings should appear when adding or updating a secret
_, err := cnf.addOrUpdateVirtualServer(vsEx)
if err != nil {
return fmt.Errorf("Error adding or updating VirtualServer %v/%v: %v", vsEx.VirtualServer.Namespace, vsEx.VirtualServer.Name, err)
}
}

if err := cnf.nginxManager.Reload(nginx.ReloadForOtherUpdate); err != nil {
return fmt.Errorf("Error when reloading NGINX when updating Secret: %v", err)
}
}
return nil
}

// addOrUpdateJWKSecretsForVirtualServer adds JWK secrets to the filesystem or updates them if they already exist.
// Returns map[jwkKeyName]jwtKeyFilename
func (cnf *Configurator) addOrUpdateJWKSecretsForVirtualServer(jwtKeys map[string]*api_v1.Secret) map[string]string {
if !cnf.isPlus {
return nil
}

jwkSecrets := make(map[string]string)

for jwkKeyName, jwkKey := range jwtKeys {
filename := cnf.addOrUpdateJWKSecret(jwkKey)
jwkSecrets[jwkKeyName] = filename
}

return jwkSecrets
}

// AddOrUpdateTLSSecret adds or updates a file with the content of the TLS secret.
Expand Down
11 changes: 10 additions & 1 deletion internal/configs/version2/http.go
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,7 @@ type Server struct {
Deny []string
LimitReqOptions LimitReqOptions
LimitReqs []LimitReq
JWTAuth *JWTAuth
PoliciesErrorReturn *Return
}

Expand Down Expand Up @@ -109,9 +110,10 @@ type Location struct {
InternalProxyPass string
Allow []string
Deny []string
PoliciesErrorReturn *Return
LimitReqOptions LimitReqOptions
LimitReqs []LimitReq
JWTAuth *JWTAuth
PoliciesErrorReturn *Return
}

// ReturnLocation defines a location for returning a fixed response.
Expand Down Expand Up @@ -266,3 +268,10 @@ type LimitReqOptions struct {
func (rl LimitReqOptions) String() string {
return fmt.Sprintf("{DryRun %v, LogLevel %q, RejectCode %q}", rl.DryRun, rl.LogLevel, rl.RejectCode)
}

// JWTAuth holds JWT authentication configuration.
type JWTAuth struct {
Secret string
Realm string
Token string
}
10 changes: 10 additions & 0 deletions internal/configs/version2/nginx-plus.virtualserver.tmpl
Original file line number Diff line number Diff line change
Expand Up @@ -131,6 +131,11 @@ server {
{{ if $rl.Delay }} delay={{ $rl.Delay }}{{ end }}{{ if $rl.NoDelay }} nodelay{{ end }};
{{ end }}

{{ with $s.JWTAuth }}
auth_jwt "{{ .Realm }}"{{ if .Token }} token={{ .Token }}{{ end }};
auth_jwt_key_file {{ .Secret }};
{{ end }}

{{ range $snippet := $s.Snippets }}
{{- $snippet }}
{{ end }}
Expand Down Expand Up @@ -220,6 +225,11 @@ server {
{{ if $rl.Delay }} delay={{ $rl.Delay }}{{ end }}{{ if $rl.NoDelay }} nodelay{{ end }};
{{ end }}

{{ with $l.JWTAuth }}
auth_jwt "{{ .Realm }}"{{ if .Token }} token={{ .Token }}{{ end }};
auth_jwt_key_file {{ .Secret }};
{{ end }}

{{ range $e := $l.ErrorPages }}
error_page {{ $e.Codes }} {{ if ne 0 $e.ResponseCode }}={{ $e.ResponseCode }}{{ end }} "{{ $e.Name }}";
{{ end }}
Expand Down
4 changes: 4 additions & 0 deletions internal/configs/version2/templates_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -136,6 +136,10 @@ var virtualServerCfg = VirtualServerConfig{
LogLevel: "error",
RejectCode: 503,
},
JWTAuth: &JWTAuth{
Realm: "My Api",
Secret: "jwk-secret",
},
Snippets: []string{"# server snippet"},
InternalRedirectLocations: []InternalRedirectLocation{
{
Expand Down
Loading

0 comments on commit 4cc4402

Please sign in to comment.