Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add JWT policy support #1154

Merged
merged 3 commits into from
Sep 22, 2020
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
49 changes: 49 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)
Dean-Coakley marked this conversation as resolved.
Show resolved Hide resolved
- [Using Policy](#using-policy)
- [Validation](#validation)
- [Structural Validation](#structural-validation)
Expand Down Expand Up @@ -189,6 +191,53 @@ 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: The feature is only available in NGINX Plus.
Dean-Coakley marked this conversation as resolved.
Show resolved Hide resolved

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"
Dean-Coakley marked this conversation as resolved.
Show resolved Hide resolved
```

> 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).
jputrino marked this conversation as resolved.
Show resolved Hide resolved

```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 with a valid JWT provided:
Dean-Coakley marked this conversation as resolved.
Show resolved Hide resolved
```
$ 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