Securing the API involves configuring OPA to use TLS, authentication, and authorization so that:

  • Traffic between OPA and clients is encrypted.
  • Clients verify the OPA API endpoint identity.
  • OPA verifies client identities.
  • Clients are only granted access to specific APIs or sections of The data Document.

HTTPS is configured by specifying TLS credentials via command line flags at startup:

  • specifies the path of the file containing the TLS certificate.
  • --tls-private-key-file=<path> specifies the path of the file containing the TLS private key.

OPA will exit immediately with a non-zero status code if only one of these flags is specified.

Note that for using TLS-based authentication, a CA cert file can be provided:

  • --tls-ca-cert-file=<path> specifies the path of the file containing the CA cert.

If provided, it will be used to validate clients’ TLS certificates when using TLS authentication (see below).

By default, OPA ignores insecure HTTP connections when TLS is enabled. To allow insecure HTTP connections in addition to HTTPS connections, provide another listening address with --addr. For example:

  1. openssl genrsa -out private.key 2048
  2. openssl req -new -x509 -sha256 -key private.key -out public.crt -days 1

2. Start OPA with TLS enabled

  1. opa run --server --log-level debug \
  2. --tls-cert-file public.crt \
  3. --tls-private-key-file private.key
  1. curl http://localhost:8181/v1/data

4. Access the API with HTTPS

  1. curl -k https://localhost:8181/v1/data

This section shows how to configure OPA to authenticate and authorize client requests. Client-side authentication of the OPA API endpoint should be handled with TLS.

Authentication and authorization allow OPA to:

  • Verify client identities.
  • Control client access to APIs and data.

Both are configured via command line flags:

  • --authentication=<scheme> specifies the authentication scheme to use.
  • --authorization=<scheme> specifies the authorization scheme to use.

By default, OPA does not perform authentication or authorization and these flags default to off.

For authentication, OPA supports:

  • : Bearer tokens are enabled by starting OPA with --authentication=token. When the token authentication mode is enabled, OPA will extract the Bearer token from incoming API requests and provide to the authorization handler. When you use the token authentication, you must configure an authorization policy that checks the tokens. If the client does not supply a Bearer token, the input.identity value will be undefined when the authorization policy is evaluated.

  • Client TLS certificates: Client TLS authentication is enabled by starting OPA with --authentication=tls. When this authentication mode is enabled, OPA will require all clients to provide a client certificate. It is verified against the CA certificate(s) provided via --tls-ca-cert-path. Upon successful verification, the input.identity value is set to the TLS certificate’s subject.

For authorization, OPA relies on policy written in Rego. Authorization is enabled by starting OPA with --authorization=basic.

When the basic authorization scheme is enabled, a minimal authorization policy must be provided on startup. The authorization policy must be structured as follows:

  1. # The "system" namespace is reserved for internal use
  2. # by OPA. Authorization policy must be defined under
  3. # system.authz as follows:
  4. package system.authz
  5. default allow = false # Reject requests by default.
  6. allow {
  7. # Logic to authorize request goes here.
  8. }

When OPA receives a request, it executes a query against the document defined data.system.authz.allow. The implementation of the policy may span multiple packages however it is recommended that administrators keep the policy under the system namespace.

If the document produced by the allow rule is true, the request is processed normally. If the document is undefined or not true, the request is rejected immediately.

OPA provides the following input document when executing the authorization policy:

  1. {
  2. # Identity established by authentication scheme.
  3. # When Bearer tokens are used, the identity is
  4. # set to the Bearer token value.
  5. "identity": "",
  6. # One of {"GET", "POST", "PUT", "PATCH", "DELETE"}.
  7. "method": "",
  8. # URL path represented as an array.
  9. # For example: /v1/data/exempli-gratia
  10. # is represented as ["v1", "data", "exampli-gratia"]
  11. "path": [...],
  12. # URL parameters represented as an object of string arrays.
  13. # For example: metrics&explain=true is represented as
  14. # {"metrics": [""], "explain": ["true"]}
  15. "params": {"...": ...},
  16. # Request headers represented as an object of string arrays.
  17. #
  18. # Example Request Headers:
  19. #
  20. # host: acmecorp.com
  21. # x-custom: secretvalue
  22. #
  23. # Example input.headers Value:
  24. #
  25. # {"Host": ["acmecorp.com"], "X-Custom": ["mysecret"]}
  26. #
  27. # Example header check:
  28. #
  29. # input.headers["X-Custom"][_] = "mysecret"
  30. #
  31. # Header keys follow canonical MIME form. The first character and any
  32. # characters following a hyphen are uppercase. The rest are lowercase.
  33. # If the header key contains space or invalid header field bytes,
  34. # no conversion is performed.
  35. "headers": {"...": [...]},
  36. # Request message body if present for applicable APIs.
  37. #
  38. # Example Request:
  39. #
  40. # POST v1/data HTTP/1.1
  41. # Content-Type: application/json
  42. #
  43. # {"input": {"action": "trade", "stock": "ACME"}}
  44. #
  45. # Example input.body Value:
  46. # {"input": {"action": "trade", "stock": "ACME"}}
  47. #
  48. #
  49. # input.body.input.stock == "ACME"
  50. #
  51. # The 'body' field is provided for the following APIs:
  52. #
  53. # * POST v1/data
  54. # * POST v0/data
  55. # * POST /
  56. "body": ...,
  57. }

At a minimum, the authorization policy should grant access to a special root identity:

  1. package system.authz
  2. default allow = false # Reject requests by default.
  3. allow { # Allow request if...
  4. "secret" == input.identity # Identity is the secret root key.
  5. }

When OPA is configured with this minimal authorization policy, requests without authentication are rejected:

Response:

  1. HTTP/1.1 401 Unauthorized
  2. Content-Type: application/json
  1. {
  2. "code": "unauthorized",
  3. "message": "request rejected by administrative policy"
  4. }

However, if Bearer token authentication is enabled and the request includes the secret from above, the request is allowed:

  1. GET /v1/policies HTTP/1.1
  2. Authorization: Bearer secret

Response:

  1. HTTP/1.1 200 OK
  2. Content-Type: application/json

Besides boolean responses, authorization policies can change the message included in the deny response. Do do that, policy decisions must yield an object response as follows:

  1. package system.authz
  2. default allow = {
  3. "allowed": false,
  4. "reason": "unauthorized resource access"
  5. }
  6. allow = { "allowed": true } { # Allow request if...
  7. "secret" == input.identity # identity is the secret root key.
  8. }
  9. allow = { "allowed": false, "reason": reason } {
  10. not input.identity
  11. reason := "no identity provided"
  12. }

When Bearer tokens are used for authentication, the policy should at minimum validate the identity:

  1. package system.authz
  2. # Tokens may defined in policy or pushed into OPA as data.
  3. tokens = {
  4. "my-secret-token-foo": {
  5. "roles": ["admin"]
  6. },
  7. "my-secret-token-bar": {
  8. "roles": ["service-1"]
  9. },
  10. "my-secret-token-baz": {
  11. "roles": ["service-2", "service-3"]
  12. }
  13. }
  14. default allow = false # Reject requests by default.
  15. allow { # Allow request if...
  16. input.identity == "secret" # Identity is the secret root key.
  17. }
  18. allow { # Allow request if...
  19. tokens[input.identity] # Identity exists in "tokens".
  20. }

To complete this example, the policy could further restrict tokens to specific documents:

  1. package system.authz
  2. # Rights may be defined in policy or pushed into OPA as data.
  3. rights = {
  4. "admin": {
  5. "path": "*"
  6. },
  7. "service-1": {
  8. "path": ["v1", "data", "exempli", "gratia"]
  9. },
  10. "service-2": {
  11. "path": ["v1", "data", "par", "example"]
  12. }
  13. }
  14. # Tokens may be defined in policy or pushed into OPA as data.
  15. tokens = {
  16. "my-secret-token-foo": {
  17. "roles": ["admin"]
  18. },
  19. "roles": ["service-1"]
  20. },
  21. "my-secret-token-baz": {
  22. "roles": ["service-2", "service-3"]
  23. }
  24. }
  25. default allow = false # Reject requests by default.
  26. allow { # Allow request if...
  27. some right
  28. identity_rights[right] # Rights for identity exist, and...
  29. right.path == "*" # Right.path is '*'.
  30. }
  31. allow { # Allow request if...
  32. some right
  33. identity_rights[right] # Rights for identity exist, and...
  34. right.path == input.path # Right.path matches input.path.
  35. }
  36. identity_rights[right] { # Right is in the identity_rights set if...
  37. some role
  38. token := tokens[input.identity] # Token exists for identity, and...
  39. role := token.roles[_] # Token has a role, and...
  40. right := rights[role] # Role has rights defined.
  41. }

TLS-based Authentication Example

To set up authentication based on TLS, we will need three certificates:

  1. the CA cert (self-signed),
  2. the server cert (signed by the CA), and
  3. the client cert (signed by the CA).

These are example invocations using openssl. Don’t use these in production, the key sizes are only good for demonstration purposes.

We also create a simple authorization policy file, called check.rego:

  1. package system.authz
  2. # client_cns may defined in policy or pushed into OPA as data.
  3. client_cns = {
  4. "my-client": true
  5. }
  6. default allow = false
  7. allow { # Allow request if
  8. split(input.identity, "=", ["CN", cn]) # the cert subject is a CN, and
  9. client_cns[cn] # the name is a known client.
  10. }

Now, we’re ready to starting the server with -authentication=tls and the certificate-related parameters:

  1. $ opa run -s \
  2. --tls-cert-file server-cert.pem \
  3. --tls-private-key-file server-key.pem \
  4. --tls-ca-cert-file ca.pem \
  5. --authentication=tls \
  6. --authorization=basic \
  7. -a https://127.0.0.1:8181 \
  8. check.rego
  9. INFO[2019-01-14T10:24:52+01:00] First line of log stream. addrs="[https://127.0.0.1:8181]" insecure_addr=

We can use curl to validate our TLS-based authentication setup:

First, we use the client certificate that was signed by the CA, and has a subject matching our authorization policy:

  1. $ curl --key client-key.pem \
  2. --cert client-cert.pem \
  3. --cacert ca.pem \
  4. --resolve opa.example.com:8181:127.0.0.1 \
  5. https://opa.example.com:8181/v1/data
  6. {"result":{}}

Note that we’re passing the CA cert to curl – this is done to have curl accept the server’s certificate, which has been signed by our CA cert.

Since we’ve setup an IP SAN, we may also curl https://127.0.0.1:8181/v1/data directly. (To keep our examples focused, we’ll do that from here on.)

Using a valid certificate whose subject will be declined by our authorization policy:

  1. $ curl --key client-key-2.pem \
  2. --cert client-cert-2.pem \
  3. --cacert ca.pem \
  4. https://127.0.0.1:8181/v1/data
  5. {
  6. "code": "unauthorized",
  7. "message": "request rejected by administrative policy"
  8. }

Finally, we’ll attempt to query without a client certificate:

  1. $ curl --cacert ca.pem https://127.0.0.1:8181/v1/data
  2. curl: (35) error:14094412:SSL routines:ssl3_read_bytes:sslv3 alert bad certificate

As you can see, TLS-based authentication disallows these request completely.

Often OPA is deployed locally to the host where the client resides (side-car or similar model). In these deployments it is ideal to only expose the API via localhost to prevent any remote clients from reaching OPA at all. The downside to this approach is that it blocks remote monitoring systems that require access to /health or /metrics.

The solution is to configure OPA with a separate diagnostic listener by providing the --diagnostic-addr flag, for example:

  1. $ opa run \
  2. -s \
  3. --addr localhost:8181 \
  4. --diagnostic-addr :8282

The configuration above would expose only /health and /metrics API’s on port 8282 while keeping the normal REST API bound to localhost:8181.

You can run a hardened OPA deployment with minimal configuration. There are a few things to keep in mind:

  • Limit API access to host-local clients executing policy queries.
  • Configure TLS (for localhost TCP) or a UNIX domain socket.
  • Do not pass credentials as command-line arguments.
  • Run OPA as a non-root user ideally inside it’s own account.

With OPA configured to fetch policies using the feature you can configure OPA with a restrictive authorization policy that only grants clients access to the default policy decision, i.e., POST /:

  1. package system.authz
  2. # Deny access by default.
  3. default allow = false
  4. # Allow anonymous access to the default policy decision.
  5. allow {
  6. input.method = "POST"
  7. input.path = [""]
  8. }
  • Authorize all API requests (--authorization=basic)
  • Listen on localhost for HTTPS (not HTTP!) connections (--addr, --tls-cert-file, --tls-private-key-file)
  • Download bundles from a remote HTTPS endpoint (--set flags and flag)