Skip to content
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
5 changes: 4 additions & 1 deletion .github/workflows/post-merge.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -81,7 +81,10 @@ jobs:
curl -LO https://storage.googleapis.com/kubernetes-release/release/$(curl -s https://storage.googleapis.com/kubernetes-release/release/stable.txt)/bin/linux/amd64/kubectl && chmod +x ./kubectl && sudo mv kubectl /usr/local/bin/

# Download and install Istio
curl -L https://git.io/getLatestIstio | ISTIO_VERSION=${ISTIO_VERSION} sh - && mv istio-${ISTIO_VERSION} /tmp && cd /tmp/istio-${ISTIO_VERSION} && for i in install/kubernetes/helm/istio-init/files/crd*yaml; do kubectl apply -f $i; done && kubectl apply -f install/kubernetes/istio-demo.yaml && cd -
curl -L https://git.io/getLatestIstio | ISTIO_VERSION=${ISTIO_VERSION} sh - && \
mv istio-${ISTIO_VERSION} /tmp && \
cd /tmp/istio-${ISTIO_VERSION} && \
bin/istioctl install -y --set profile=demo
kubectl -n istio-system wait --for=condition=available --timeout=600s --all deployment

# Install bats
Expand Down
5 changes: 5 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -13,3 +13,8 @@ site.tar.gz

# runtime artifacts
policies

# cert files
opa-envoy.crt
opa-envoy.key
root.crt
191 changes: 171 additions & 20 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -162,7 +162,7 @@ The OPA-Envoy plugin supports the following configuration fields:
| `plugins["envoy_ext_authz_grpc"].addr` | No | Set listening address of Envoy External Authorization gRPC server. This must match the value configured in the Envoy config. Default: `:9191`. |
| `plugins["envoy_ext_authz_grpc"].path` | No | Specifies the hierarchical policy decision path. The policy decision can either be a `boolean` or an `object`. If boolean, `true` indicates the request should be allowed and `false` indicates the request should be denied. If the policy decision is an object, it **must** contain the `allowed` key set to either `true` or `false` to indicate if the request is allowed or not respectively. It can optionally contain a `headers` field to send custom headers to the downstream client or upstream. An optional `body` field can be included in the policy decision to send a response body data to the downstream client. Also an optional `http_status` field can be included to send a HTTP response status code to the downstream client other than `403 (Forbidden)`. Default: `envoy/authz/allow`.|
| `plugins["envoy_ext_authz_grpc"].dry-run` | No | Configures the Envoy External Authorization gRPC server to unconditionally return an `ext_authz.CheckResponse.Status` of `google_rpc.Status{Code: google_rpc.OK}`. Default: `false`. |
|`plugins["envoy_ext_authz_grpc"].enable-reflection`| No | Enables gRPC server reflection on the Envoy External Authorization gRPC server. Default: `false`. |
| `plugins["envoy_ext_authz_grpc"].enable-reflection` | No | Enables gRPC server reflection on the Envoy External Authorization gRPC server. Default: `false`. |

If the configuration does not specify the `path` field, `envoy/authz/allow` will be considered as the default policy
decision path. `data.envoy.authz.allow` will be the name of the policy decision to query in the default case.
Expand Down Expand Up @@ -229,7 +229,7 @@ The policy also restricts an `admin` user, in this case `bob` from creating an e
The policy uses the `io.jwt.decode_verify` builtin function to parse and verify the JWT containing information
about the user making the request.

```ruby
```rego
package envoy.authz

import input.attributes.request.http as http_request
Expand Down Expand Up @@ -277,6 +277,65 @@ action_allowed {

The `input` value defined for your policy will resemble the JSON below:

```json
{
"attributes": {
"source": {
"address": {
"socketAddress": {
"address": "172.17.0.1",
"portValue": 61402
}
}
},
"destination": {
"address": {
"socketAddress": {
"address": "172.17.06",
"portValue": 8000
}
}
},
"request": {
"time": "2020-11-20T09:47:47.722473Z",
"http": {
"id":"13519049518330544501",
"method": "POST",
"headers": {
":authority":"192.168.99.206:30164",
":method":"POST",
":path":"/people?lang=en",
"accept": "*/*",
"authorization":"Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJyb2xlIjoiYWRtaW4iLCJzdWIiOiJZbTlpIiwibmJmIjoxNTE0ODUxMTM5LCJleHAiOjE2NDEwODE1Mzl9.WCxNAveAVAdRCmkpIObOTaSd0AJRECY2Ch2Qdic3kU8",
"content-length":"41",
"content-type":"application/json",
"user-agent":"curl/7.54.0",
"x-forwarded-proto":"http",
"x-request-id":"7bca5c86-bf55-432c-b212-8c0f1dc999ec"
},
"host":"192.168.99.206:30164",
"path":"/people?lang=en",
"protocol":"HTTP/1.1",
"body":"{\"firstname\":\"Charlie\", \"lastname\":\"Opa\"}",
"size":41
}
},
"metadataContext": {}
},
"parsed_body":{"firstname": "Charlie", "lastname": "Opa"},
"parsed_path":["people"],
"parsed_query": {"lang": ["en"]},
"truncated_body": false,
"version": {
"encoding":"protojson",
"ext_authz":"v3"
}
}
```
Note that this is the input [using the v3 API](#envoy-xds-v2-and-v2).

<details><summary>See here for an example of v2 input</summary>

```json
{
"attributes":{
Expand Down Expand Up @@ -331,16 +390,21 @@ The `input` value defined for your policy will resemble the JSON below:
"parsed_body":{"firstname": "Charlie", "lastname": "Opa"},
"parsed_path":["people"],
"parsed_query": {"lang": ["en"]},
"truncated_body": false
"truncated_body": false,
"version": {
"encoding":"encoding/json",
"ext_authz":"v2"
}
}
```
</details>

The `parsed_path` field in the input is generated from the `path` field in the HTTP request which is included in the
Envoy External Authorization `CheckRequest` message type. This field provides the request path as a string array which
can help policy authors perform pattern matching on the HTTP request path. The below sample policy allows anyone to
access the path `/people`.

```ruby
```rego
package envoy.authz

default allow = false
Expand All @@ -354,7 +418,7 @@ The `parsed_query` field in the input is also generated from the `path` field in
the HTTP url query as a map of string array. The below sample policy allows anyone to access the path
`/people?lang=en&id=1&id=2`.

```ruby
```rego
package envoy.authz

default allow = false
Expand All @@ -370,7 +434,7 @@ The `parsed_body` field in the input is generated from the `body` field in the H
Envoy External Authorization `CheckRequest` message type. This field contains the deserialized JSON request body which
can then be used in a policy as shown below.

```ruby
```rego
package envoy.authz

default allow = false
Expand All @@ -391,7 +455,7 @@ The `allow` rule in the below policy when queried generates an `object` that pro
(ie. `allowed` or `denied`) along with some headers, body data and HTTP status which will be included in the response
that is sent back to the downstream client or upstream.

```ruby
```rego
package envoy.authz

default allow = {
Expand Down Expand Up @@ -482,7 +546,8 @@ Envoy can be configured to pass validated JWT payload data into the `ext_authz`
and `payload_in_metadata`.

### Example Envoy Configuration
```ruby

```yaml
http_filters:
- name: envoy.filters.http.jwt_authn
typed_config:
Expand All @@ -499,9 +564,11 @@ http_filters:
```

### Example OPA Input

This will result in something like the following dictionary being added to `input.attributes` (some common fields have
been excluded for brevity):
```ruby

```
"metadata_context": {
"filter_metadata": {
"envoy.filters.http.jwt_authn": {
Expand All @@ -519,14 +586,101 @@ been excluded for brevity):

This JWT data can be accessed in OPA policy like this:

```ruby
```rego
jwt_payload = input.attributes.metadata_context.filter_metadata["envoy.filters.http.jwt_authn"].verified_jwt

allow {
jwt_payload.email == "alice@example.com"
}
```

## Envoy xDS v2 and v2

This plugin exposes both versions. For v3 requests, the [specified JSON mapping for protobuf](https://developers.google.com/protocol-buffers/docs/proto3#json)
is used for making the incoming `envoy.service.auth.v3.CheckRequest` available in `input`.
It differs from the encoding
used for v2 requests:

In v3, all keys are lower camelcase. Also, needless nesting of oneof values is removed.

For example, source address data that looks like this in v2,
```
"source": {
"address": {
"Address": {
"SocketAddress": {
"PortSpecifier": {
"PortValue": 59052
},
"address": "127.0.0.1"
}
}
}
}
```

becomes, in v3,
```
"source": {
"address": {
"socketAddress": {
"address": "127.0.0.1",
"portValue": 59052
}
}
}
```

The following table shows the rego code for common data, in v2 and v3:


| information | rego v2 | rego v3 |
|---------------------|----------|---------|
| source address | `input.attributes.source.address.Address.SocketAddress.address` | `input.attributes.source.address.socketAddress.address`|
| source port | `input.attributes.source.address.Address.SocketAddress.PortSpecifier.PortValue` | `input.attributes.source.address.socketAddress.portValue`|
| destination address | `input.attributes.destination.address.Address.SocketAddress.address` | `input.attributes.destination.address.socketAddress.address`|
| destination port | `input.attributes.destination.address.Address.SocketAddress.PortSpecifier.PortValue` | `input.attributes.destination.address.socketAddress.portValue`|
| dynamic metadata | `input.attributes.metadata_context.filter_metadata` | `input.attributes.metadataContext.filterMetadata` |

Due to those differences, it's important to know which version is used when writing policies.
Thus this information is passed into the OPA evaluation under `input.version`, where you'll either
find, for v2,

```rego
input.version == { "ext_authz": "v2", "encoding": "encoding/json" }
```

or, for v3,

```rego
input.version == { "ext_authz": "v3", "encoding": "protojson" }
```

This information can also be used to create policies that are compatible with both versions and
encodings.

To have Envoy use the v3 version of the service, it will need to be configured to do so.
The http_filters entry should look like this (minimal version):
```yaml
http_filters:
- name: envoy.ext_authz
typed_config:
'@type': type.googleapis.com/envoy.extensions.filters.http.ext_authz.v3.ExtAuthz
transport_api_version: V3
grpc_service:
google_grpc: # or envoy_grpc
target_uri: "127.0.0.1:9191"
```

Note that further settings are required to have (raw) request bodies forwarded to the ext authz
service.

For the use in Istio, _at least_ Istio 1.7.0 is **required** to use a v3 ExtAuthz filter, [see
the 1.7.0 release notes](https://istio.io/latest/news/releases/1.7.x/announcing-1.7/upgrade-notes/#envoyfilter-syntax-change) for details.
<!-- NOTE(sr): https://github.com/istio/istio/commit/e0e53ee9190049a0126490f3b9b59cb3f3210620 -->

When using grpcurl (see below) you can choose with which version to interact.

## gRPC Server Reflection Usage

This section provides examples of interacting with the Envoy External Authorization gRPC server using the `grpcurl` tool.
Expand All @@ -541,13 +695,14 @@ This section provides examples of interacting with the Envoy External Authorizat

```bash
envoy.service.auth.v2.Authorization
envoy.service.auth.v3.Authorization
grpc.reflection.v1alpha.ServerReflection
```

* Invoke RPC on the server
* Invoke a v3 Check RPC on the server

```bash
$ grpcurl -plaintext -import-path ./proto/ -proto ./proto/envoy/service/auth/v2/external_auth.proto -d '
$ grpcurl -plaintext -d '
{
"attributes": {
"request": {
Expand All @@ -557,16 +712,16 @@ This section provides examples of interacting with the Envoy External Authorizat
}
}
}
}' localhost:9191 envoy.service.auth.v2.Authorization/Check
}' localhost:9191 envoy.service.auth.v3.Authorization/Check
```

Output:

```bash
```
{
"status": {
"code": 0
},

},
"okResponse": {
"headers": [
{
Expand All @@ -580,10 +735,6 @@ This section provides examples of interacting with the Envoy External Authorizat
}
```

The `-proto` and `-import-path` flags tell `grpcurl` the relevant proto source file and the folder from which
dependencies can be imported respectively. These flags need to be provided as the Envoy External Authorization gRPC
server does not support reflection. See this [issue](https://github.com/grpc/grpc-go/issues/1873) for details.

## Dependencies

Dependencies are managed with [Modules](https://github.com/golang/go/wiki/Modules).
Expand Down
45 changes: 45 additions & 0 deletions build/gen-tls-certs.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
#!/usr/bin/env bash
# This script is used to generate TLS certs that can be used with opa-envoy-plugin built
# with Go 1.15+. (That version of Go declines CN=<domain name> for server identification,
# but requires proper SNI settings, using subjectAltName (SAN).
#
# After running the script, the output of `base64 opa-envoy.crt` and `base64 opa-envoy.key`
# need to be pasted into examples/istio/quick_start.yaml:
# - The cert and key goes into tls.crt and tls.key of the server-cert Secret,
# - The cert also goes into clientConfig.caBundle of the webhook 'opa-istio-admission-controller'.

cat > v3.txt <<- EOF
keyUsage = critical, digitalSignature, keyEncipherment, dataEncipherment, keyAgreement
extendedKeyUsage = serverAuth
subjectKeyIdentifier=hash
authorityKeyIdentifier=keyid,issuer
subjectAltName = DNS:admission-controller.opa-istio.svc
EOF

openssl req -x509 \
-subj "/CN=OPA Envoy plugin" \
-nodes \
-newkey rsa:4096 \
-days 1826 \
-keyout root.key \
-out root.crt
openssl genrsa -out opa-envoy.key 4096
openssl req -new \
-key opa-envoy.key \
-subj "/CN=opa-envoy" \
-reqexts SAN \
-config <(cat /etc/ssl/openssl.cnf \
<(printf "\n[SAN]\nsubjectAltName=DNS:admissin-controller.opa-istio.svc")) \
-sha256 \
-out opa-envoy.csr
openssl x509 -req \
-extfile v3.txt \
-CA root.crt \
-CAkey root.key \
-CAcreateserial \
-days 1825 \
-sha256 \
-in opa-envoy.csr \
-out opa-envoy.crt

rm v3.txt opa-envoy.csr root.key root.srl
Loading