Sample for envoy with WASM filter where the filter will invoke an external GRPC service.
THe full flow is like this:
client ->
(jwt_header) ->
[
envoy.filters.network.http_connection_manager ->
envoy.filters.http.jwt_authn ->
envoy.filters.http.wasm ->
]
-> (api_req) -> (jwt_header) gRPC server -> (api_resp)
-> [
envoy.filters.http.router
]
-> upstream_server
Basically, the client transmits a jwt bearer authorization token to envoy.
Envoy will first validate the JWT header using its native jwt_authn
filter
Once validated, the decoded JWT claims are emitted as metadata to a wasm filter
The wasm filter will extract the sub
field metadata and use that in an rpc call to an external gRPC server.
The external grpcServer will respond back isAdmin: true
if the sub
field is Alice, otherwise the value is false.
Envoy will ultimately send the isAdmin
header to the upstream server.
The upstream server is httpbin.org which will just display the headers it received.
This flow is very similar to how external authorization servers can be configured (shown below). However, this repo is just a sample which demonstrates how to configure/develop an filter.
I’ve done building the wasm filter the hard way…you should consider just taking a look at wasme
The sample filter is also just a copy of the wasm-cc
sandbox filter
References:
Envoy External Authorization server (envoy.ext_authz) with OPA HelloWorld
Redefining extensibility in proxies - introducing WebAssembly to Envoy and Istio
To use this sample, you’ll need:
1.17
docker cp `docker create envoyproxy/envoy-dev:latest`:/usr/local/bin/envoy /tmp/
/tmp/envoy --version
version: 27c507ee0ae51713dbdf66a24cb9a47f46700b78/1.20.0-dev/Clean/RELEASE/BoringSSL
protoc
You can find the source here
First clone envoy and build the filter
git clone https://github.com/envoyproxy/envoy.git
rm -rf envoy/examples/wasm-cc/
cp -R wasm-cc envoy/examples
Now build the modified filter
cd envoy
bazel build //examples/wasm-cc:envoy_filter_http_wasm_example.wasm
Add to /etc/hosts
127.0.0.1 grpc.domain.com
This is the address for the grpc server (this is just for convenience to make the SNI match
(you don’t need to do this but i got lazy with the envoy config…TODO: configure envoy better)
/tmp/envoy -c envoy-wasm.yaml -l debug
cd grpc_server/
# (optional) recompile protos
# /usr/local/bin/protoc --go_out=. --go_opt=paths=source_relative --descriptor_set_out=echo/echo.proto.pb --go-grpc_out=. --go-grpc_opt=paths=source_relative echo/echo.proto
# run server
go run greeter_server/grpc_server.go --tlsCert grpc_server_crt.pem --tlsKey grpc_server_key.pem --grpcport :50051
## test client
# go run greeter_client/grpc_client.go --host localhost:50051 --servername grpc.domain.com --cacert ../certs/tls-ca.crt
We’re going to use curl to emit two different pregenerated JWTs
Alice’s JWT includes her name in the sub
field
{
"alg": "RS256",
"kid": "DHFbpoIUqrY8t2zpA2qXfCmr5VO5ZEr4RzHU_-envvQ",
"typ": "JWT"
}.
{
"exp": 1609408793,
"iat": 1609108793,
"iss": "new-issuer@secure.istio.io",
"sub": "alice@domain.com"
}
And bob includes his
{
"alg": "RS256",
"kid": "DHFbpoIUqrY8t2zpA2qXfCmr5VO5ZEr4RzHU_-envvQ",
"typ": "JWT"
}.
{
"exp": 1609408787,
"iat": 1609108787,
"iss": "new-issuer@secure.istio.io",
"sub": "bob@domain.com"
}
Now use their names to invoke
You can generate your own JWTs using istio’s handy scripts here:
wget --no-verbose https://raw.githubusercontent.com/istio/istio/release-1.10/security/tools/jwt/samples/gen-jwt.py
wget --no-verbose https://raw.githubusercontent.com/istio/istio/release-1.10/security/tools/jwt/samples/key.pem
python3 gen-jwt.py -iss foo.bar -aud sal -expire 100000 key.pem
JWK URI = "https://raw.githubusercontent.com/istio/istio/release-1.10/security/tools/jwt/samples/jwks.json";
curl -v -H "host: http.domain.com" --resolve http.domain.com:8080:127.0.0.1 \
-H "Authorization: Bearer `cat jwts/alice.txt`" \
-H "User: sal" http://http.domain.com:8080/get
> GET /get HTTP/1.1
> Host: http.domain.com
> User-Agent: curl/7.72.0
> Accept: */*
> Authorization: Bearer eyJhbGciOiJSUzI1NiIsImtpZCI6IkRIRmJwb0lVcXJZOHQyenBBMnFYZkNtcjVWTzVaRXI0UnpIVV8tZW52dlEiLCJ0eXAiOiJKV1QifQ.eyJleHAiOjE2MDk0MDg3OTMsImlhdCI6MTYwOTEwODc5MywiaXNzIjoibmV3LWlzc3VlckBzZWN1cmUuaXN0aW8uaW8iLCJzdWIiOiJhbGljZUBkb21haW4uY29tIn0.WeRcHxVsKZAKD1uu-1efYhUwH9K5cWr6-Doo-CVulAhPol8oXazmZ-6wMUnqtOcWh5YOevVzUhIF8jUDibIHgsvksSprXrZf8BAkC68ctb1O0eDTlhKw0fdS41PedmBWnTESkBYFgEAKDeS4Re3bIN2irPVfSTldxqXepkl8K6R_R_Gnuyqxaie16JmIADMJ1unRbd4rcW3grXdYF4Dc7EvCpinQuQJQOdaNn1mQ2JrckTnrr8R6xf6pLpEDjAKGqeNKQdRjAAUdHSZqIylHMwIgcWAVrTFWz9TmUrmQmSqReJRa4SdAGBaTCKL9UeBiqyGYpEZn1wCfMj-ukwNQvA
> User: sal
< HTTP/1.1 200 OK
< date: Mon, 28 Dec 2020 00:55:39 GMT
< server: envoy
< access-control-allow-origin: *
< access-control-allow-credentials: true
< x-envoy-upstream-service-time: 34
< x-wasm-custom: FOO
< content-type: text/plain; charset=utf-8
< transfer-encoding: chunked
{
"args": {},
"headers": {
"Accept": "*/*",
"Content-Length": "0",
"Host": "http.domain.com",
"Isadmin": "true",
"User": "sal",
"User-Agent": "curl/7.72.0",
"X-Amzn-Trace-Id": "Root=1-5fe92d0b-079e277b0d79542f3c3e7af6",
"X-Envoy-Expected-Rq-Timeout-Ms": "15000"
},
"origin": "69.250.44.79",
"url": "http://http.domain.com/get"
}
Hello, world
The authorization bearer token is the client token Alice sends.
"Isadmin": "true"
: the auth header is decoded by envoy. The sub
field is given to the gRPC server. If the sub=Alice, then the gRPC server adds this header back in the response. The wasm filter will append isAdmin:true
to the upstram.
x-wasm-custom: FOO
: this is a header value the wasm filter returns back to the client.
Bob
If bob tries to use his jwt token in the same way, the header he sees is isAdmin: false
curl -v -H "host: http.domain.com" --resolve http.domain.com:8080:127.0.0.1 \
-H "Authorization: Bearer `cat jwts/bob.txt`" \
-H "User: sal" http://http.domain.com:8080/get
> GET /get HTTP/1.1
> Host: http.domain.com
> User-Agent: curl/7.72.0
> Accept: */*
> Authorization: Bearer eyJhbGciOiJSUzI1NiIsImtpZCI6IkRIRmJwb0lVcXJZOHQyenBBMnFYZkNtcjVWTzVaRXI0UnpIVV8tZW52dlEiLCJ0eXAiOiJKV1QifQ.eyJleHAiOjE2MDk0MDg3ODcsImlhdCI6MTYwOTEwODc4NywiaXNzIjoibmV3LWlzc3VlckBzZWN1cmUuaXN0aW8uaW8iLCJzdWIiOiJib2JAZG9tYWluLmNvbSJ9.Q3QPnOkqhQN_BrDDmmSpugLRVbcyoXrXgl7NqtlUrZeef2tMQh7ycJhg4z73J6iw49v7ye2CgMrjScHTUVaGgPItItYAVfTwGXC-VBekqnrhCRhZ57ou3vJHjT7xADL9qvwahBDKjpGji8uzsvHsHZXBgiVxVh_5lYBLt6PcoVgqHAgn_uNTnE0EJJgV7Vs39k73wtxqYkuvpZdMdaWw1gLOmFhxSu2yqLHNtfLZIPyVZxyrK1KtAw9yFIDmsIEtLOpjdIqKIJ5Nh48OeN5LNhz0r2Alrj7nM_d11FYc-0k9R58vRE7SgIJNvzUKlcptkjHb0K23DoIw8QnhFFHGfg
> User: sal
< HTTP/1.1 200 OK
< date: Mon, 28 Dec 2020 01:01:31 GMT
< server: envoy
< access-control-allow-origin: *
< access-control-allow-credentials: true
< x-envoy-upstream-service-time: 41
< x-wasm-custom: FOO
< content-type: text/plain; charset=utf-8
< transfer-encoding: chunked
{
"args": {},
"headers": {
"Accept": "*/*",
"Content-Length": "0",
"Host": "http.domain.com",
"Isadmin": "false",
"User": "sal",
"User-Agent": "curl/7.72.0",
"X-Amzn-Trace-Id": "Root=1-5fe92e6b-47819b1739fd76e15353398b",
"X-Envoy-Expected-Rq-Timeout-Ms": "15000"
},
"origin": "69.250.44.79",
"url": "http://http.domain.com/get"
}
Hello, world
A couple of notes about the flow:
envoy.filters.network.http_connection_manager
will remove the isAdmin
header if its sent in unilaterally by the client.
see internal_only_headers: - isadmin
setting
envoy.filters.http.jwt_authn
will validate the inboud jwt and emit the claims as dynamic metadata
see payload_in_metadata: "my_payload"
envoy.filters.http.wasm
will read the config file for the gRPC cluster name:
configuration:
"@type": "type.googleapis.com/google.protobuf.StringValue"
value: |
{
"clustername": "grpc.domain.com",
}
the configuration file is actually defined as a proto struct here:
wasm-cc/echo/echo.proto
:
// this proto represents configuration for the example filter
message Config {
string clustername = 1;
}
The proto messages the wasm filter uses to make the outbound call is also defined and compiled
with the wasm filter. see wasm-cc/echo/echo.proto
This site supports webmentions. Send me a mention via this form.