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

HTTP basic auth support #2269

Merged
merged 7 commits into from
Jun 30, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
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
8 changes: 8 additions & 0 deletions deployments/common/crds/k8s.nginx.org_policies.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,14 @@ spec:
type: array
items:
type: string
basicAuth:
description: 'BasicAuth holds HTTP Basic authentication configuration policy status: preview'
type: object
properties:
realm:
type: string
secret:
type: string
egressMTLS:
description: EgressMTLS defines an Egress MTLS policy.
type: object
Expand Down
8 changes: 8 additions & 0 deletions deployments/helm-chart/crds/k8s.nginx.org_policies.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,14 @@ spec:
type: array
items:
type: string
basicAuth:
description: 'BasicAuth holds HTTP Basic authentication configuration policy status: preview'
type: object
properties:
realm:
type: string
secret:
type: string
egressMTLS:
description: EgressMTLS defines an Egress MTLS policy.
type: object
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -136,6 +136,8 @@ The table below summarizes the available annotations.
|``nginx.org/hsts-max-age`` | ``hsts-max-age`` | Sets the value of the ``max-age`` directive of the HSTS header. | ``2592000`` (1 month) | |
|``nginx.org/hsts-include-subdomains`` | ``hsts-include-subdomains`` | Adds the ``includeSubDomains`` directive to the HSTS header. | ``False`` | |
|``nginx.org/hsts-behind-proxy`` | ``hsts-behind-proxy`` | Enables HSTS based on the value of the ``http_x_forwarded_proto`` request header. Should only be used when TLS termination is configured in a load balancer (proxy) in front of the Ingress Controller. Note: to control redirection from HTTP to HTTPS configure the ``nginx.org/redirect-to-https`` annotation. | ``False`` | |
|``nginx.org/basic-auth-secret`` | N/A | Specifies a Secret resource with a user list for HTTP Basic authentication. | N/A | |
|``nginx.org/basic-auth-realm`` | N/A | Specifies a realm. | N/A | |
|``nginx.com/jwt-key`` | N/A | Specifies a Secret resource with keys for validating JSON Web Tokens (JWTs). | N/A | [Support for JSON Web Tokens (JWTs)](https://github.com/nginxinc/kubernetes-ingress/tree/v2.2.2/examples/jwt). |
|``nginx.com/jwt-realm`` | N/A | Specifies a realm. | N/A | [Support for JSON Web Tokens (JWTs)](https://github.com/nginxinc/kubernetes-ingress/tree/v2.2.2/examples/jwt). |
|``nginx.com/jwt-token`` | N/A | Specifies a variable that contains a JSON Web Token. | By default, a JWT is expected in the ``Authorization`` header as a Bearer Token. | [Support for JSON Web Tokens (JWTs)](https://github.com/nginxinc/kubernetes-ingress/tree/v2.2.2/examples/jwt). |
Expand Down
31 changes: 31 additions & 0 deletions docs/content/configuration/policy-resource.md
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ spec:
|``accessControl`` | The access control policy based on the client IP address. | [accessControl](#accesscontrol) | No |
|``ingressClassName`` | Specifies which Ingress Controller must handle the Policy resource. | ``string`` | No |
|``rateLimit`` | The rate limit policy controls the rate of processing requests per a defined key. | [rateLimit](#ratelimit) | No |
|``basicAuth`` | The basic auth policy configures NGINX to authenticate client requests using HTTP Basic authentication credentials. | [basicAuth](#basic-auth) | No |
|``jwt`` | The JWT policy configures NGINX Plus to authenticate client requests using JSON Web Tokens. | [jwt](#jwt) | No |
|``ingressMTLS`` | The IngressMTLS policy configures client certificate verification. | [ingressMTLS](#ingressmtls) | No |
|``egressMTLS`` | The EgressMTLS policy configures upstreams authentication and certificate verification. | [egressMTLS](#egressmtls) | No |
Expand Down Expand Up @@ -132,6 +133,36 @@ 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).

### BasicAuth

The basic auth policy configures NGINX to authenticate cllient requests using the [HTTP Basic authentication scheme](https://developer.mozilla.org/en-US/docs/Web/HTTP/Authentication).

For example, the following policy will reject all requests that do not include a valid username/password combination in the HTTP header `Authentication`
```yaml
basicAuth:
secret: htpasswd-secret
realm: "My API"
```

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

{{% table %}}
|Field | Description | Type | Required |
| ---| ---| ---| --- |
|``secret`` | The name of the Kubernetes secret that stores the Htpasswd configuration. It must be in the same namespace as the Policy resource. The secret must be of the type ``nginx.org/htpasswd``, and the config must be stored in the secret under the key ``htpasswd``, otherwise the secret will be rejected as invalid. | ``string`` | Yes |
|``realm`` | The realm for the basic authentication. | ``string`` | No |
{{% /table %}}

#### BasicAuth Merging Behavior

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

### JWT

> Note: This feature is only available in NGINX Plus.
Expand Down
116 changes: 116 additions & 0 deletions examples/basic-auth/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
# Support for HTTP Basic Authentication

NGINX supports authenticating requests with [ngx_http_auth_basic_module](https://nginx.org/en/docs/http/ngx_http_auth_basic_module.html).

The Ingress controller provides the following 2 annotations for configuring Basic Auth validation:

* Required: ```nginx.org/basic-auth-secret: "secret"``` -- specifies a Secret resource with a htpasswd user list. The htpasswd must be stored in the `htpasswd` data field. The type of the secret must be `nginx.org/htpasswd`.
* Optional: ```nginx.org/basic-auth-realm: "realm"``` -- specifies a realm.

```

## Example 1: The Same Htpasswd for All Paths

In the following example we enable HTTP Basic authentication for the cafe-ingress Ingress for all paths using the same htpasswd `cafe-htpasswd`:
```yaml
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: cafe-ingress
annotations:
nginx.org/basic-auth-secret: "cafe-passwd"
nginx.org/basic-auth-realm: "Cafe App"
spec:
tls:
- hosts:
- cafe.example.com
secretName: cafe-secret
rules:
- host: cafe.example.com
http:
paths:
- path: /tea
backend:
service:
name: tea-svc
port:
number: 80
- path: /coffee
backend:
service:
name: coffee-svc
port:
number: 80
```
* The keys must be deployed separately in the Secret `cafe-jwk`.
* The realm is `Cafe App`.

## Example 2: a Separate Htpasswd Per Path

In the following example we enable Basic Auth validation for the [mergeable Ingresses](../mergeable-ingress-types) with a separate Basic Auth user:password list per path:

* Master:
```yaml
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: cafe-ingress-master
annotations:
kubernetes.io/ingress.class: "nginx"
nginx.org/mergeable-ingress-type: "master"
spec:
tls:
- hosts:
- cafe.example.com
secretName: cafe-secret
rules:
- host: cafe.example.com
```

* Tea minion:
```yaml
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: cafe-ingress-tea-minion
annotations:
nginx.org/mergeable-ingress-type: "minion"
nginx.org/basic-auth-secret: "tea-passwd"
nginx.org/basic-auth-realm: "Tea"
spec:
rules:
- host: cafe.example.com
http:
paths:
- path: /tea
pathType: Prefix
backend:
service:
name: tea-svc
port:
number: 80
```

* Coffee minion:
```yaml
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: cafe-ingress-coffee-minion
annotations:
nginx.org/mergeable-ingress-type: "minion"
nginx.org/basic-auth-secret: "coffee-passwd"
nginx.org/basic-auth-realm: "Coffee"
spec:
rules:
- host: cafe.example.com
http:
paths:
- path: /coffee
pathType: Prefix
backend:
service:
name: coffee-svc
port:
number: 80
```
10 changes: 10 additions & 0 deletions internal/configs/annotations.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,9 @@ import (
// JWTKeyAnnotation is the annotation where the Secret with a JWK is specified.
const JWTKeyAnnotation = "nginx.com/jwt-key"

// BasicAuthSecretAnnotation is the annotation where the Secret with the HTTP basic user list
const BasicAuthSecretAnnotation = "nginx.org/basic-auth-secret" // #nosec G101

// AppProtectPolicyAnnotation is where the NGINX App Protect policy is specified
const AppProtectPolicyAnnotation = "appprotect.f5.com/app-protect-policy"

Expand Down Expand Up @@ -299,6 +302,13 @@ func parseAnnotations(ingEx *IngressEx, baseCfgParams *ConfigParams, isPlus bool
}
}

if basicSecret, exists := ingEx.Ingress.Annotations[BasicAuthSecretAnnotation]; exists {
svvac marked this conversation as resolved.
Show resolved Hide resolved
cfgParams.BasicAuthSecret = basicSecret
}
if basicRealm, exists := ingEx.Ingress.Annotations["nginx.org/basic-auth-realm"]; exists {
cfgParams.BasicAuthRealm = basicRealm
}

if values, exists := ingEx.Ingress.Annotations["nginx.org/listen-ports"]; exists {
ports, err := ParsePortList(values)
if err != nil {
Expand Down
3 changes: 3 additions & 0 deletions internal/configs/config_params.go
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,9 @@ type ConfigParams struct {
JWTRealm string
JWTToken string

BasicAuthSecret string
BasicAuthRealm string

Ports []int
SSLPorts []int

Expand Down
26 changes: 23 additions & 3 deletions internal/configs/configurator.go
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,9 @@ const WildcardSecretName = "wildcard"
// JWTKeyKey is the key of the data field of a Secret where the JWK must be stored.
const JWTKeyKey = "jwk"

// HtpasswdFileKey is the key of the data field of a Secret where the HTTP basic authorization list must be stored
const HtpasswdFileKey = "htpasswd"

// CAKey is the key of the data field of a Secret where the cert must be stored.
const CAKey = "ca.crt"

Expand Down Expand Up @@ -268,12 +271,15 @@ func (cnf *Configurator) addOrUpdateIngress(ingEx *IngressEx) (Warnings, error)
cnf.updateDosResource(ingEx.DosEx)
dosResource := getAppProtectDosResource(ingEx.DosEx)

// LocalSecretStore will not set Path if the secret is not on the filesystem.
// However, NGINX configuration for an Ingress resource, to handle the case of a missing secret,
// relies on the path to be always configured.
if jwtKey, exists := ingEx.Ingress.Annotations[JWTKeyAnnotation]; exists {
// LocalSecretStore will not set Path if the secret is not on the filesystem.
// However, NGINX configuration for an Ingress resource, to handle the case of a missing secret,
// relies on the path to be always configured.
ingEx.SecretRefs[jwtKey].Path = cnf.nginxManager.GetFilenameForSecret(ingEx.Ingress.Namespace + "-" + jwtKey)
}
if basicAuth, exists := ingEx.Ingress.Annotations[BasicAuthSecretAnnotation]; exists {
ingEx.SecretRefs[basicAuth].Path = cnf.nginxManager.GetFilenameForSecret(ingEx.Ingress.Namespace + "-" + basicAuth)
}

isMinion := false
nginxCfg, warnings := generateNginxCfg(ingEx, apResources, dosResource, isMinion, cnf.cfgParams, cnf.isPlus, cnf.IsResolverConfigured(),
Expand Down Expand Up @@ -317,10 +323,16 @@ func (cnf *Configurator) addOrUpdateMergeableIngress(mergeableIngs *MergeableIng
if jwtKey, exists := mergeableIngs.Master.Ingress.Annotations[JWTKeyAnnotation]; exists {
mergeableIngs.Master.SecretRefs[jwtKey].Path = cnf.nginxManager.GetFilenameForSecret(mergeableIngs.Master.Ingress.Namespace + "-" + jwtKey)
}
if basicAuth, exists := mergeableIngs.Master.Ingress.Annotations[BasicAuthSecretAnnotation]; exists {
mergeableIngs.Master.SecretRefs[basicAuth].Path = cnf.nginxManager.GetFilenameForSecret(mergeableIngs.Master.Ingress.Namespace + "-" + basicAuth)
}
for _, minion := range mergeableIngs.Minions {
if jwtKey, exists := minion.Ingress.Annotations[JWTKeyAnnotation]; exists {
minion.SecretRefs[jwtKey].Path = cnf.nginxManager.GetFilenameForSecret(minion.Ingress.Namespace + "-" + jwtKey)
}
if basicAuth, exists := minion.Ingress.Annotations[BasicAuthSecretAnnotation]; exists {
minion.SecretRefs[basicAuth].Path = cnf.nginxManager.GetFilenameForSecret(minion.Ingress.Namespace + "-" + basicAuth)
}
}

nginxCfg, warnings := generateNginxCfgForMergeableIngresses(mergeableIngs, apResources, dosResource, cnf.cfgParams, cnf.isPlus,
Expand Down Expand Up @@ -650,6 +662,12 @@ func (cnf *Configurator) addOrUpdateJWKSecret(secret *api_v1.Secret) string {
return cnf.nginxManager.CreateSecret(name, data, nginx.JWKSecretFileMode)
}

func (cnf *Configurator) addOrUpdateHtpasswdSecret(secret *api_v1.Secret) string {
name := objectMetaToFileName(&secret.ObjectMeta)
data := secret.Data[HtpasswdFileKey]
return cnf.nginxManager.CreateSecret(name, data, nginx.HtpasswdSecretFileMode)
}

// AddOrUpdateResources adds or updates configuration for resources.
func (cnf *Configurator) AddOrUpdateResources(resources ExtendedResources) (Warnings, error) {
allWarnings := newWarnings()
Expand Down Expand Up @@ -1496,6 +1514,8 @@ func (cnf *Configurator) AddOrUpdateSecret(secret *api_v1.Secret) string {
return cnf.addOrUpdateCASecret(secret)
case secrets.SecretTypeJWK:
return cnf.addOrUpdateJWKSecret(secret)
case secrets.SecretTypeHtpasswd:
return cnf.addOrUpdateHtpasswdSecret(secret)
case secrets.SecretTypeOIDC:
// OIDC ClientSecret is not required on the filesystem, it is written directly to the config file.
return ""
Expand Down
34 changes: 34 additions & 0 deletions internal/configs/ingress.go
Original file line number Diff line number Diff line change
Expand Up @@ -187,6 +187,12 @@ func generateNginxCfg(ingEx *IngressEx, apResources *AppProtectResources, dosRes
allWarnings.Add(warnings)
}

if !isMinion && cfgParams.BasicAuthSecret != "" {
basicAuth, warnings := generateBasicAuthConfig(ingEx.Ingress, ingEx.SecretRefs, &cfgParams)
server.BasicAuth = basicAuth
allWarnings.Add(warnings)
}

var locations []version1.Location
healthChecks := make(map[string]version1.HealthCheck)

Expand Down Expand Up @@ -237,6 +243,12 @@ func generateNginxCfg(ingEx *IngressEx, apResources *AppProtectResources, dosRes
allWarnings.Add(warnings)
}

if isMinion && cfgParams.BasicAuthSecret != "" {
basicAuth, warnings := generateBasicAuthConfig(ingEx.Ingress, ingEx.SecretRefs, &cfgParams)
loc.BasicAuth = basicAuth
allWarnings.Add(warnings)
}

locations = append(locations, loc)

if loc.Path == "/" {
Expand Down Expand Up @@ -327,6 +339,28 @@ func generateJWTConfig(owner runtime.Object, secretRefs map[string]*secrets.Secr
return jwtAuth, redirectLocation, warnings
}

func generateBasicAuthConfig(owner runtime.Object, secretRefs map[string]*secrets.SecretReference, cfgParams *ConfigParams) (*version1.BasicAuth, Warnings) {
warnings := newWarnings()

secretRef := secretRefs[cfgParams.BasicAuthSecret]
var secretType api_v1.SecretType
if secretRef.Secret != nil {
secretType = secretRef.Secret.Type
}
if secretType != "" && secretType != secrets.SecretTypeHtpasswd {
warnings.AddWarningf(owner, "Basic auth secret %s is of a wrong type '%s', must be '%s'", cfgParams.BasicAuthSecret, secretType, secrets.SecretTypeHtpasswd)
} else if secretRef.Error != nil {
warnings.AddWarningf(owner, "Basic auth secret %s is invalid: %v", cfgParams.BasicAuthSecret, secretRef.Error)
}

basicAuth := &version1.BasicAuth{
Secret: secretRef.Path,
Realm: cfgParams.BasicAuthRealm,
}

return basicAuth, warnings
}

func addSSLConfig(server *version1.Server, owner runtime.Object, host string, ingressTLS []networking.IngressTLS,
secretRefs map[string]*secrets.SecretReference, isWildcardEnabled bool,
) Warnings {
Expand Down
Loading