class: center, middle, inverse
.right[Shane Soh] .right[Senior Software Engineer] .right[Centre for Strategic Infocomm Technologies]
Centre for Strategic Infocomm Technologies (CSIT) is an agency in the Ministry of Defence that builds technologies to safeguard the national security interests of Singapore.
Our team builds platforms and infrastructure that support a wide range of mission-critical operations, such as in counter-terrorism and computer network defence.
-
Classified low-trust environments where complex access control and audit logging are required for internal APIs
-
Increasingly more microservices but still have many monolithic legacy systems running on VMs
-
Need API security that is easy to implement and to reason with
Thus Open Policy Agent (OPA) and Envoy
Open Policy Agent is a lightweight general-purpose policy engine that lets you specify policy as code and use APIs to offload policy decision-making from your software.
Envoy is an open source edge and service proxy designed for cloud-native applications.
For our purpose, Envoy is used to delegate authorization decisions to OPA, allowing/denying requests to the service based on OPA's policy decisions.
-
Teams are expected to deploy Envoy and OPA alongside their service
-
All incoming requests first go to Envoy which checks with OPA whether each request should be allowed through
-
Service can simply assume any request that arrives is authorized as per OPA policies
.footnote[github.com/shanesoh/envoy-opa-compose]
docker-compose.yml
...
envoy:
build: ./compose/envoy
ports:
- "8080:80"
volumes:
- ./envoy.yaml:/config/envoy.yaml
environment:
- DEBUG_LEVEL=info
- SERVICE_NAME=app # should match name of underlying service
- SERVICE_PORT=80
app:
image: kennethreitz/httpbin:latest
...
- Built Envoy image does environment variable substitution for
envoy.yaml
- To simplify adoption as Envoy config can be rather complex
- httpbin as a mock service
.footnote[github.com/shanesoh/envoy-opa-compose]
docker-compose.yml
...
opa:
image: openpolicyagent/opa:0.26.0-envoy
volumes:
- ./policy.rego:/config/policy.rego
command:
- "run"
- "--log-level=debug"
- "--log-format=json-pretty"
- "--server"
- "--set=plugins.envoy_ext_authz_grpc.path=envoy/authz/allow"
- "--set=decision_logs.console=true"
- "/config/policy.rego"
...
- OPA-Envoy image uses opa-envoy-plugin which extends OPA with
a gRPC server that implements the Envoy External Authorization (
ext_authz
) API.
.footnote[github.com/shanesoh/envoy-opa-compose]
policy.rego
package envoy.authz
import input.attributes.request.http as http_request
default allow = false
allow = response {
http_request.method == "GET"
response := {
"allowed": true,
"headers": {"X-Auth-User": "1234"}
}
}
- Policy decision can be boolean or an object (as in this case)
- Toy policy to only allow GET requests and add additional header for
X-Auth-User
.footnote[github.com/shanesoh/envoy-opa-compose]
# This is allowed
$ curl -X GET http://localhost:8080/anything
{
...
"headers": {
...
"X-Auth-User": "1234",
...
},
...
"method": "GET",
}
# This gets denied
$ curl -X POST http://localhost:8080/anything
.footnote[github.com/shanesoh/envoy-opa-compose]
In our actual setup we make authorization decisions primarily using OAuth2/OIDC access tokens
# ...truncated...
default allow = false
token = payload {
# Access token in `Authorization: Bearer <token>` header
[_, encoded] := split(http_request.headers.authorization, " ")
[_, payload, _] := io.jwt.decode(encoded)
}
allow = response {
# Check access token contains `read` permission to `myapp`
token.resource_access["myapp"].roles[_] == "read"
response := {
"allowed": true,
"headers": {
"X-User-Id": token.user_id,
"X-Given-Name": token.given_name
}
}
}
--
- Manage policy as code
- Written in Rego
- Declarative; relatively easy to read, write and test
--
- Decouple policy decision-making from policy enforcement
- Authorization logic written outside of service instead of it peppered all over the code
- Separation of concerns: Developers focus on writing application logic assuming authorization is handled
- Policies can be reloaded without restarting underlying service
- Facilitates discoverability, reuse and governance
- Authorization logic written outside of service instead of it peppered all over the code
--
- Centralised management APIs
- Policy distribution via bundles APIs
- Especially universal policies
- Collection of telemetry
- e.g. Decision logs (that are clearly separated from application logs)
- Policy distribution via bundles APIs
- Filters to implement common functionalities
- External Authorization (
ext_authz
) to OPA - But also JWT Authentication (
jwt_authn
) filter to validate access tokens
- External Authorization (
--
- Purpose-built for sidecar usage
- Completely agnostic to upstream service
- With OPA, useful for tacking on access control to services that don't have them
- Containerised for Docker or k8s
- At the same time easy to run on the host for legacy applications
- Possible to auto inject as k8s sidecars
- Completely agnostic to upstream service
--
- Future-proofing
- Currently east-west traffic goes through API gateway which provides functionalities like traffic control and logging centrally
- Eases potential adoption of service mesh: Move functionalities into Envoy sidecars and use Istio as control plane
class: left, middle, inverse