Skip to content

Commit

Permalink
Support APIKey authentication (#5580)
Browse files Browse the repository at this point in the history
* always load njs module
Signed-off-by: Haywood Shannon <[email protected]>

Signed-off-by: Haywood Shannon <[email protected]>

* accept api key policy yaml
Signed-off-by: Haywood Shannon <[email protected]>

Signed-off-by: Haywood Shannon <[email protected]>

* [pre-commit.ci] auto fixes from pre-commit.com hooks

for more information, see https://pre-commit.ci

* add njs and nginx config

* move js import to http

* update schema and allow update after NIC starts
Signed-off-by: Haywood Shannon <[email protected]>

Signed-off-by: Haywood Shannon <[email protected]>

* [pre-commit.ci] auto fixes from pre-commit.com hooks

for more information, see https://pre-commit.ci

* remove hardcoded variable

* [pre-commit.ci] auto fixes from pre-commit.com hooks

for more information, see https://pre-commit.ci

* add updated njs

* move js set outside location

* use nginx.org/apikey secret type
Signed-off-by: Haywood Shannon <[email protected]>

Signed-off-by: Haywood Shannon <[email protected]>

* [pre-commit.ci] auto fixes from pre-commit.com hooks

for more information, see https://pre-commit.ci

* simplify njs

* make query params work

* clean up template files

* add api key secret validation to reject duplicated keys, remove repeated maps in config, remove reject code, add unit tests
Signed-off-by: Haywood Shannon <[email protected]>

Signed-off-by: Haywood Shannon <[email protected]>

* [pre-commit.ci] auto fixes from pre-commit.com hooks

for more information, see https://pre-commit.ci

* fix linting, remove unused structs, update crds and codegen
Signed-off-by: Haywood Shannon <[email protected]>

Signed-off-by: Haywood Shannon <[email protected]>

* add unit tests, unique map names, add validate apikey location block to conf only if api key policy is used
Signed-off-by: Haywood Shannon <[email protected]>

Signed-off-by: Haywood Shannon <[email protected]>

* add python tests for vs and vsr

* fix dockerfile merge

* add wait until pods are ready

* update error message
Signed-off-by: Haywood Shannon <[email protected]>

Signed-off-by: Haywood Shannon <[email protected]>

* test setting same namespace

* custom objects

* add crd print

* add unit tests
Signed-off-by: Haywood Shannon <[email protected]>

Signed-off-by: Haywood Shannon <[email protected]>

* Add example readme for apikey auth policy

* clean up

* further cleanup

* clean up test

* add unit tests, clean up code
Signed-off-by: Haywood Shannon <[email protected]>

Signed-off-by: Haywood Shannon <[email protected]>

* remove logs, refactor, add tests
Signed-off-by: Haywood Shannon <[email protected]>

Signed-off-by: Haywood Shannon <[email protected]>

* remove logs
Signed-off-by: Haywood Shannon <[email protected]>

Signed-off-by: Haywood Shannon <[email protected]>

* add api key auth to telemetry

---------

Signed-off-by: Haywood Shannon <[email protected]>
Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
Co-authored-by: Jim Ryan <[email protected]>
  • Loading branch information
3 people authored Jun 17, 2024
1 parent 8752533 commit dbba378
Show file tree
Hide file tree
Showing 48 changed files with 2,563 additions and 52 deletions.
1 change: 1 addition & 0 deletions build/scripts/common.sh
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ if [ -z "${BUILD_OS##*plus*}" ]; then
PLUS=-plus
fi

mkdir -p /etc/nginx/njs/ && cp -a /tmp/internal/configs/njs/* /etc/nginx/njs/
mkdir -p /var/lib/nginx /etc/nginx/secrets /etc/nginx/stream-conf.d
setcap 'cap_net_bind_service=+eip' /usr/sbin/nginx 'cap_net_bind_service=+eip' /usr/sbin/nginx-debug
setcap -v 'cap_net_bind_service=+eip' /usr/sbin/nginx 'cap_net_bind_service=+eip' /usr/sbin/nginx-debug
Expand Down
19 changes: 19 additions & 0 deletions config/crd/bases/k8s.nginx.org_policies.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,25 @@ spec:
type: string
type: array
type: object
apiKey:
description: APIKey defines an API Key policy.
properties:
clientSecret:
type: string
suppliedIn:
description: SuppliedIn defines the locations API Key should be
supplied in.
properties:
header:
items:
type: string
type: array
query:
items:
type: string
type: array
type: object
type: object
basicAuth:
description: |-
BasicAuth holds HTTP Basic authentication configuration
Expand Down
19 changes: 19 additions & 0 deletions deploy/crds.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -269,6 +269,25 @@ spec:
type: string
type: array
type: object
apiKey:
description: APIKey defines an API Key policy.
properties:
clientSecret:
type: string
suppliedIn:
description: SuppliedIn defines the locations API Key should be
supplied in.
properties:
header:
items:
type: string
type: array
query:
items:
type: string
type: array
type: object
type: object
basicAuth:
description: |-
BasicAuth holds HTTP Basic authentication configuration
Expand Down
1 change: 1 addition & 0 deletions docs/content/overview/product-telemetry.md
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ These are the data points collected and reported by NGINX Ingress Controller:
- **IngressAnnotations** List of Ingress annotations managed by NGINX Ingress Controller
- **AccessControlPolicies** Number of AccessControl policies.
- **RateLimitPolicies** Number of RateLimit policies.
- **APIKeyPolicies** Number of API Key Auth policies.
- **JWTAuthPolicies** Number of JWTAuth policies.
- **BasicAuthPolicies** Number of BasicAuth policies.
- **IngressMTLSPolicies** Number of IngressMTLS policies.
Expand Down
122 changes: 122 additions & 0 deletions examples/custom-resources/api-key/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
# API Key Authentication

NGINX supports authenticating requests with
[ngx_http_auth_request_module](https://nginx.org/en/docs/http/ngx_http_auth_request_module.html). In this example, we deploy
a web application, configure load balancing for it via a VirtualServer, and apply an API Key Auth policy.

## Prerequisites

1. Follow the [installation](https://docs.nginx.com/nginx-ingress-controller/installation/installation-with-manifests/)
instructions to deploy the Ingress Controller. In this example we will be using a snippet to turn the policy off on a specific path so ensure that the `enable-snippets` flag is set.
1. Save the public IP address of the Ingress Controller into a shell variable:

```console
IC_IP=XXX.YYY.ZZZ.III
```

1. Save the HTTP port of the Ingress Controller into a shell variable:

```console
IC_HTTP_PORT=<port number>
```

## Step 1 - Deploy a Web Application

Create the application deployment and service:

```console
kubectl apply -f cafe.yaml
```

## Step 2 - Deploy the API Key Auth Secret

Create a secret of type `nginx.org/apikey` with the name `api-key-client-secret` that will be used for authorization on the server level.

This secret will contain a mapping of client IDs to base64 encoded API Keys.

```console
kubectl apply -f api-key-secret.yaml
```

## Step 3 - Deploy the API Key Auth Policy

Create a policy with the name `api-key-policy` that references the secret from the previous step in the clientSecret field.
Provide an array of headers and queries in the header and query fields of the suppliedIn field, indicating where the API key can be sent

```console
kubectl apply -f api-key-policy.yaml
```

## Step 4 - Configure Load Balancing

Create a VirtualServer resource for the web application:

```console
kubectl apply -f cafe-virtual-server.yaml
```

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

## Step 5 - Test the Configuration

If you attempt to access the application without providing a valid API Key in a expected header or query param for that VirtualServer:

```console
curl --resolve cafe.example.com:$IC_HTTP_PORT:$IC_IP http://cafe.example.com:$IC_HTTP_PORT/
```

```text
<html>
<head><title>401 Authorization Required</title></head>
<body>
<center><h1>401 Authorization Required</h1></center>
<hr><center>nginx/1.21.5</center>
</body>
</html>
```

If you attempt to access the application providing an incorrect API Key in an expected header or query param for that VirtualServer:

```console
curl --resolve cafe.example.com:$IC_HTTP_PORT:$IC_IP -H "X-header-name: wrongpassword" http://cafe.example.com:$IC_HTTP_PORT/coffee
```

```text
<html>
<head><title>403 Forbidden</title></head>
<body>
<center><h1>403 Forbidden</h1></center>
<hr><center>nginx/1.27.0</center>
</body>
</html>
```

If you provide a valid API Key in an a header or query defined in the policy, your request will succeed:

```console
curl --resolve cafe.example.com:$IC_HTTPS_PORT:$IC_IP -H "X-header-name: password" https://cafe.example.com:$IC_HTTPS_PORT/coffee
```

```text
Server address: 10.244.0.6:8080
Server name: coffee-56b44d4c55-vjwxd
Date: 13/Jun/2024:13:12:17 +0000
URI: /coffee
Request ID: 4feedb3265a0430a1f58831d016e846d
```

If you attempt to access the /tea path, the request will be allowed without an API Key, because the auth_request directive is turned off for that path with a location snippet:

```console
curl --resolve cafe.example.com:$IC_HTTP_PORT:$IC_IP http://cafe.example.com:$IC_HTTP_PORT/tea
```

```text
Server address: 10.244.0.5:8080
Server name: tea-596697966f-dmq7t
Date: 13/Jun/2024:13:16:46 +0000
URI: /tea
Request ID: 26e6d7dd0272eca82f31f33bf90698c9
```

Additionally you can set [error pages](https://docs.nginx.com/nginx-ingress-controller/configuration/virtualserver-and-virtualserverroute-resources/#errorpage) to handle the 401 and 403 responses.
12 changes: 12 additions & 0 deletions examples/custom-resources/api-key/api-key-policy.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
apiVersion: k8s.nginx.org/v1
kind: Policy
metadata:
name: api-key-policy
spec:
apiKey:
suppliedIn:
header:
- "X-header-name"
query:
- "queryName"
clientSecret: api-key-client-secret
8 changes: 8 additions & 0 deletions examples/custom-resources/api-key/api-key-secret.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
apiVersion: v1
kind: Secret
metadata:
name: api-key-client-secret
type: nginx.org/apikey
data:
client1: cGFzc3dvcmQ= # password
client2: YW5vdGhlci1wYXNzd29yZA== # another-password
8 changes: 8 additions & 0 deletions examples/custom-resources/api-key/cafe-secret.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
apiVersion: v1
kind: Secret
metadata:
name: cafe-secret
type: kubernetes.io/tls
data:
tls.crt: LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tCk1JSURMakNDQWhZQ0NRREFPRjl0THNhWFdqQU5CZ2txaGtpRzl3MEJBUXNGQURCYU1Rc3dDUVlEVlFRR0V3SlYKVXpFTE1Ba0dBMVVFQ0F3Q1EwRXhJVEFmQmdOVkJBb01HRWx1ZEdWeWJtVjBJRmRwWkdkcGRITWdVSFI1SUV4MApaREViTUJrR0ExVUVBd3dTWTJGbVpTNWxlR0Z0Y0d4bExtTnZiU0FnTUI0WERURTRNRGt4TWpFMk1UVXpOVm9YCkRUSXpNRGt4TVRFMk1UVXpOVm93V0RFTE1Ba0dBMVVFQmhNQ1ZWTXhDekFKQmdOVkJBZ01Ba05CTVNFd0h3WUQKVlFRS0RCaEpiblJsY201bGRDQlhhV1JuYVhSeklGQjBlU0JNZEdReEdUQVhCZ05WQkFNTUVHTmhabVV1WlhoaApiWEJzWlM1amIyMHdnZ0VpTUEwR0NTcUdTSWIzRFFFQkFRVUFBNElCRHdBd2dnRUtBb0lCQVFDcDZLbjdzeTgxCnAwanVKL2N5ayt2Q0FtbHNmanRGTTJtdVpOSzBLdGVjcUcyZmpXUWI1NXhRMVlGQTJYT1N3SEFZdlNkd0kyaloKcnVXOHFYWENMMnJiNENaQ0Z4d3BWRUNyY3hkam0zdGVWaVJYVnNZSW1tSkhQUFN5UWdwaW9iczl4N0RsTGM2SQpCQTBaalVPeWwwUHFHOVNKZXhNVjczV0lJYTVyRFZTRjJyNGtTa2JBajREY2o3TFhlRmxWWEgySTVYd1hDcHRDCm42N0pDZzQyZitrOHdnemNSVnA4WFprWldaVmp3cTlSVUtEWG1GQjJZeU4xWEVXZFowZXdSdUtZVUpsc202OTIKc2tPcktRajB2a29QbjQxRUUvK1RhVkVwcUxUUm9VWTNyemc3RGtkemZkQml6Rk8yZHNQTkZ4MkNXMGpYa05MdgpLbzI1Q1pyT2hYQUhBZ01CQUFFd0RRWUpLb1pJaHZjTkFRRUxCUUFEZ2dFQkFLSEZDY3lPalp2b0hzd1VCTWRMClJkSEliMzgzcFdGeW5acS9MdVVvdnNWQTU4QjBDZzdCRWZ5NXZXVlZycTVSSWt2NGxaODFOMjl4MjFkMUpINnIKalNuUXgrRFhDTy9USkVWNWxTQ1VwSUd6RVVZYVVQZ1J5anNNL05VZENKOHVIVmhaSitTNkZBK0NuT0Q5cm4yaQpaQmVQQ0k1ckh3RVh3bm5sOHl3aWozdnZRNXpISXV5QmdsV3IvUXl1aTlmalBwd1dVdlVtNG52NVNNRzl6Q1Y3ClBwdXd2dWF0cWpPMTIwOEJqZkUvY1pISWc4SHc5bXZXOXg5QytJUU1JTURFN2IvZzZPY0s3TEdUTHdsRnh2QTgKN1dqRWVxdW5heUlwaE1oS1JYVmYxTjM0OWVOOThFejM4Zk9USFRQYmRKakZBL1BjQytHeW1lK2lHdDVPUWRGaAp5UkU9Ci0tLS0tRU5EIENFUlRJRklDQVRFLS0tLS0K
tls.key: LS0tLS1CRUdJTiBSU0EgUFJJVkFURSBLRVktLS0tLQpNSUlFb3dJQkFBS0NBUUVBcWVpcCs3TXZOYWRJN2lmM01wUHJ3Z0pwYkg0N1JUTnBybVRTdENyWG5LaHRuNDFrCkcrZWNVTldCUU5semtzQndHTDBuY0NObzJhN2x2S2wxd2k5cTIrQW1RaGNjS1ZSQXEzTVhZNXQ3WGxZa1YxYkcKQ0pwaVJ6ejBza0lLWXFHN1BjZXc1UzNPaUFRTkdZMURzcGRENmh2VWlYc1RGZTkxaUNHdWF3MVVoZHErSkVwRwp3SStBM0kreTEzaFpWVng5aU9WOEZ3cWJRcCt1eVFvT05uL3BQTUlNM0VWYWZGMlpHVm1WWThLdlVWQ2cxNWhRCmRtTWpkVnhGbldkSHNFYmltRkNaYkp1dmRySkRxeWtJOUw1S0Q1K05SQlAvazJsUkthaTAwYUZHTjY4NE93NUgKYzMzUVlzeFR0bmJEelJjZGdsdEkxNURTN3lxTnVRbWF6b1Z3QndJREFRQUJBb0lCQVFDUFNkU1luUXRTUHlxbApGZlZGcFRPc29PWVJoZjhzSStpYkZ4SU91UmF1V2VoaEp4ZG01Uk9ScEF6bUNMeUw1VmhqdEptZTIyM2dMcncyCk45OUVqVUtiL1ZPbVp1RHNCYzZvQ0Y2UU5SNThkejhjbk9SVGV3Y290c0pSMXBuMWhobG5SNUhxSkpCSmFzazEKWkVuVVFmY1hackw5NGxvOUpIM0UrVXFqbzFGRnM4eHhFOHdvUEJxalpzVjdwUlVaZ0MzTGh4bndMU0V4eUZvNApjeGI5U09HNU9tQUpvelN0Rm9RMkdKT2VzOHJKNXFmZHZ5dGdnOXhiTGFRTC94MGtwUTYyQm9GTUJEZHFPZVBXCktmUDV6WjYvMDcvdnBqNDh5QTFRMzJQem9idWJzQkxkM0tjbjMyamZtMUU3cHJ0V2wrSmVPRmlPem5CUUZKYk4KNHFQVlJ6NWhBb0dCQU50V3l4aE5DU0x1NFArWGdLeWNrbGpKNkY1NjY4Zk5qNUN6Z0ZScUowOXpuMFRsc05ybwpGVExaY3hEcW5SM0hQWU00MkpFUmgySi9xREZaeW5SUW8zY2czb2VpdlVkQlZHWTgrRkkxVzBxZHViL0w5K3l1CmVkT1pUUTVYbUdHcDZyNmpleHltY0ppbS9Pc0IzWm5ZT3BPcmxEN1NQbUJ2ek5MazRNRjZneGJYQW9HQkFNWk8KMHA2SGJCbWNQMHRqRlhmY0tFNzdJbUxtMHNBRzR1SG9VeDBlUGovMnFyblRuT0JCTkU0TXZnRHVUSnp5K2NhVQprOFJxbWRIQ2JIelRlNmZ6WXEvOWl0OHNaNzdLVk4xcWtiSWN1YytSVHhBOW5OaDFUanNSbmU3NFowajFGQ0xrCmhIY3FIMHJpN1BZU0tIVEU4RnZGQ3haWWRidUI4NENtWmlodnhicFJBb0dBSWJqcWFNWVBUWXVrbENkYTVTNzkKWVNGSjFKelplMUtqYS8vdER3MXpGY2dWQ0thMzFqQXdjaXowZi9sU1JxM0hTMUdHR21lemhQVlRpcUxmZVpxYwpSMGlLYmhnYk9jVlZrSkozSzB5QXlLd1BUdW14S0haNnpJbVpTMGMwYW0rUlk5WUdxNVQ3WXJ6cHpjZnZwaU9VCmZmZTNSeUZUN2NmQ21mb09oREN0enVrQ2dZQjMwb0xDMVJMRk9ycW40M3ZDUzUxemM1em9ZNDR1QnpzcHd3WU4KVHd2UC9FeFdNZjNWSnJEakJDSCtULzZzeXNlUGJKRUltbHpNK0l3eXRGcEFOZmlJWEV0LzQ4WGY2ME54OGdXTQp1SHl4Wlp4L05LdER3MFY4dlgxUE9ucTJBNWVpS2ErOGpSQVJZS0pMWU5kZkR1d29seHZHNmJaaGtQaS80RXRUCjNZMThzUUtCZ0h0S2JrKzdsTkpWZXN3WEU1Y1VHNkVEVXNEZS8yVWE3ZlhwN0ZjanFCRW9hcDFMU3crNlRYcDAKWmdybUtFOEFSek00NytFSkhVdmlpcS9udXBFMTVnMGtKVzNzeWhwVTl6WkxPN2x0QjBLSWtPOVpSY21Vam84UQpjcExsSE1BcWJMSjhXWUdKQ2toaVd4eWFsNmhZVHlXWTRjVmtDMHh0VGwvaFVFOUllTktvCi0tLS0tRU5EIFJTQSBQUklWQVRFIEtFWS0tLS0tCg==
26 changes: 26 additions & 0 deletions examples/custom-resources/api-key/cafe-virtual-server.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
apiVersion: k8s.nginx.org/v1
kind: VirtualServer
metadata:
name: cafe
spec:
host: cafe.example.com
tls:
secret: cafe-secret
policies:
- name: api-key-policy
upstreams:
- name: coffee
service: coffee-svc
port: 80
- name: tea
service: tea-svc
port: 80
routes:
- path: /coffee
action:
pass: coffee
- path: /tea
location-snippets: |
auth_request off;
action:
pass: tea
65 changes: 65 additions & 0 deletions examples/custom-resources/api-key/cafe.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
apiVersion: apps/v1
kind: Deployment
metadata:
name: coffee
spec:
replicas: 2
selector:
matchLabels:
app: coffee
template:
metadata:
labels:
app: coffee
spec:
containers:
- name: coffee
image: nginxdemos/nginx-hello:plain-text
ports:
- containerPort: 8080
---
apiVersion: v1
kind: Service
metadata:
name: coffee-svc
spec:
ports:
- port: 80
targetPort: 8080
protocol: TCP
name: http
selector:
app: coffee
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: tea
spec:
replicas: 1
selector:
matchLabels:
app: tea
template:
metadata:
labels:
app: tea
spec:
containers:
- name: tea
image: nginxdemos/nginx-hello:plain-text
ports:
- containerPort: 8080
---
apiVersion: v1
kind: Service
metadata:
name: tea-svc
spec:
ports:
- port: 80
targetPort: 8080
protocol: TCP
name: http
selector:
app: tea
3 changes: 3 additions & 0 deletions internal/configs/configurator.go
Original file line number Diff line number Diff line change
Expand Up @@ -1893,6 +1893,9 @@ func (cnf *Configurator) AddOrUpdateSecret(secret *api_v1.Secret) string {
case secrets.SecretTypeOIDC:
// OIDC ClientSecret is not required on the filesystem, it is written directly to the config file.
return ""
case secrets.SecretTypeAPIKey:
// APIKey ClientSecret is not required on the filesystem, it is written directly to the config file.
return ""
default:
return cnf.addOrUpdateTLSSecret(secret)
}
Expand Down
26 changes: 26 additions & 0 deletions internal/configs/njs/apikey_auth.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
const c = require('crypto')

function hash(r) {
const header_query_value = r.variables.header_query_value;
const hashed_value = c.createHash('sha256').update(header_query_value).digest('hex');
return hashed_value;
}

function validate(r) {
const client_name_map = r.variables['apikey_auth_local_map'];
const client_name = r.variables[client_name_map];
const header_query_value = r.variables.header_query_value;

if (!header_query_value) {
r.return(401, "401")
}
else if (!client_name) {
r.return(403, "403")
}
else {
r.return(204, "204");
}

}

export default { validate, hash };
5 changes: 3 additions & 2 deletions internal/configs/version1/nginx-plus.tmpl
Original file line number Diff line number Diff line change
Expand Up @@ -27,9 +27,7 @@ load_module modules/ngx_fips_check_module.so;
{{$value}}{{end}}
{{- end}}

{{- if .OIDC}}
load_module modules/ngx_http_js_module.so;
{{- end}}

events {
worker_connections {{.WorkerConnections}};
Expand All @@ -41,6 +39,9 @@ http {
map_hash_max_size {{.MapHashMaxSize}};
map_hash_bucket_size {{.MapHashBucketSize}};

js_import /etc/nginx/njs/apikey_auth.js;
js_set $apikey_auth_hash apikey_auth.hash;

{{- if .HTTPSnippets}}
{{range $value := .HTTPSnippets}}
{{$value}}{{end}}
Expand Down
6 changes: 6 additions & 0 deletions internal/configs/version1/nginx.tmpl
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,8 @@ load_module modules/ngx_http_opentracing_module.so;
{{$value}}{{end}}
{{- end}}

load_module modules/ngx_http_js_module.so;

events {
worker_connections {{.WorkerConnections}};
}
Expand All @@ -30,6 +32,10 @@ http {
map_hash_max_size {{.MapHashMaxSize}};
map_hash_bucket_size {{.MapHashBucketSize}};


js_import /etc/nginx/njs/apikey_auth.js;
js_set $apikey_auth_hash apikey_auth.hash;

{{- if .HTTPSnippets}}
{{range $value := .HTTPSnippets}}
{{$value}}{{end}}
Expand Down
Loading

0 comments on commit dbba378

Please sign in to comment.