Skip to main content

Securing the Outway

The Outway is a proxy that sets up a connection to a service according to the FSC specification. By default the Outway runs without TLS and without authorization.

There are two independent options for securing the Outway:

  • TLS, so that clients can trust they are actually talking to the Outway.
  • A Policy Decision Point (PDP), which authorizes the client using the Outway.

The options do not depend on each other. You can enable either one, both, or neither, depending on what your deployment needs. The sections below describe each option on its own.

Configuring TLS on the Outway

This guide covers enabling TLS on the Outway's client-facing listener — the hop between your own applications and the Outway.

What you get

  • TLS 1.2 as the minimum version, with the Mozilla intermediate cipher list
  • Server authentication only. The Outway never requests or verifies a client certificate.

Certificate requirements

  • PEM-encoded certificate and private key, loaded as a standard key pair at startup.
  • If your leaf is signed by an intermediate, concatenate the chain into the certificate file, leaf first.
  • The SAN must include the hostname clients use to reach the Outway.
  • Do not reuse the FSC group certificate here. It works technically, but it ties your FSC identity to an internal-facing listener. Issue a separate certificate from your internal CA (or a public one, if the Outway is reachable from outside).

Key file permissions

The Outway warns at startup when a private key is world-writable, world-readable, or executable. 0640 and 0600 both pass; 0644 does not. This check runs on the group and internal keys only, but hold the server key to the same standard.


Kubernetes (Helm)

Below you will see two examples. One uses inline PEM to provide the certificate and private key, the other uses an existing secret. Only the relevant values are included.

Using an inline PEM:

https:
enabled: true
keyPEM: |
-----BEGIN RSA PRIVATE KEY-----
................................................................
-----END RSA PRIVATE KEY-----
certificatePEM: |
-----BEGIN CERTIFICATE-----
................................................................
-----END CERTIFICATE-----

Using an existing secret:

https:
enabled: true
existingSecret:
name: outway-server-tls
keyCertificate: tls.crt
keyPrivateKey: tls.key

Relevant Helm chart values:

ValueDefaultDescription
https.enabledfalseenables TLS, when true a server certificate and key must be provided
https.existingSecret.name(empty)The name of the secret containing the certificate and private key
https.existingSecret.keyCertificatetls.crtThe key in the secret that contains the certificate
https.existingSecret.keyPrivateKeytls.keyThe key in the secret that contains the private key
https.keyPEM(empty)inline PEM-encoded key
https.certificatePEM(empty)inline PEM-encoded certificate

What the chart changes for you

Turning on https.enabled flips several things at once:

  • Container port 8080 (named http) becomes 8443 (named https).
  • LISTEN_ADDRESS becomes 0.0.0.0:8443 and LISTEN_HTTPS becomes 1.
  • The Secret is mounted read-only at /certificate-server, and TLS_SERVER_CERT / TLS_SERVER_KEY point into it.
  • The Service exposes service.httpsPort (default 443) named https instead of service.httpPort named http.

Docker

The Outway image docker.io/federatedserviceconnectivity/outway runs as UID 10001. We want to mount the certificate material as read-only and make sure that UID 10001 can read it.

Restrict the file permissions:

chmod 0640 server-key.pem server-cert.pem

Set the ownership to UID 10001:

sudo chown 10001:10001 server-key.pem server-cert.pem

Docker Compose

Below you see a Docker Compose setup that has enabled TLS on the Outway, only the relevant environment flags are included.

This guide expects server-key.pem and server-cert.pem to be present in the folder ./certs

services:
outway:
image: docker.io/federatedserviceconnectivity/outway:latest
ports:
- "8443:8443"
volumes:
- ./certs:/certs:ro
environment:
LISTEN_ADDRESS: 0.0.0.0:8443
LISTEN_HTTPS: "1"
TLS_SERVER_CERT: /certs/server-cert.pem
TLS_SERVER_KEY: /certs/server-key.pem

Relevant environment values

Environment variableDefaultDescription
LISTEN_HTTPSfalseenables TLS, when true TLS_SERVER_CERT and TLS_SERVER_KEY are required
TLS_SERVER_CERT(empty)Path to a PEM-encoded certificate
TLS_SERVER_KEY(empty)Path to a PEM-encoded key
LISTEN_ADDRESS127.0.0.1:8080Listen address of the Outway

Verifying

Check the startup log of the Outway first. You want to see:

starting HTTPS server on 0.0.0.0:8443

If it says starting HTTP server, LISTEN_HTTPS did not take effect. In this case you should check the LISTEN_HTTPS flag.

Then, from a client:

# Inspect the presented chain
openssl s_client -connect outway.example.org:8443 -servername outway.example.org </dev/null

# Confirm the listener answers over TLS
curl -v --cacert internal-ca.pem https://outway.example.org:8443/health/live

A 200 from /health/live over HTTPS means the listener is up with a chain your client trusts. If you do not receive a 200 status code you should check the relevant environment values.

Configuring a Policy Decision Point (PDP)

It is possible to configure a Policy Decision Point (PDP) on the Outway. The rest of the documentation calls the PDP an authorization service; the two (PDP and authorization service) are the same thing. Setup authorization describes what the Outway sends it, the interface it must implement, and how to do the same for an Inway. This section walks through a concrete example.

The PDP will be called for each request going through the Outway, the PDP decides if the request is authorized and the Outway enforces the decision of the PDP.

In the example below we will use Open Policy Agent (OPA) as a PDP. We will create a policy that expects an API key to be present in the HTTP header Authorization. If the API key is present and matches with the expected API key very-secret-api-key, the request will be authorized.

The Policy:

package httpapi.outway

import rego.v1

default allow := {
"allowed": false,
"status": {"reason": "permission denied authorization header does not contain a valid api key"},
}

allow := {"allowed": true, "status": {"reason": reason}} if {
valid_api_key
reason := "authorization header contains a valid api key"
}

valid_api_key if {
input.headers["Authorization"][0] == "very-secret-api-key"
}

In our example OPA runs on https://opa-service.example.com and it provides TLS using a certificate signed by an internal CA.

Kubernetes (Helm)

In our case OPA provides TLS using a certificate that is signed by an internal CA. The certificate of the CA must be present as a Kubernetes secret.

Kubernetes Secret containing the CA certificate:

apiVersion: v1
kind: Secret
metadata:
name: ca-root-certificate
type: Opaque
stringData:
ca.crt: |
-----BEGIN CERTIFICATE-----
................................................................
-----END CERTIFICATE-----

Outway chart values, only the relevant values are included:

config:
authorizationService:
enabled: true
url: https://opa-service.example.com/v1/data/httpapi/outway/allow
caCertificatePEMExistingSecret:
name: ca-root-certificate
key: ca.crt

Relevant Helm chart values

ValueDefaultDescription
config.authorizationService.enabledfalseWhen true, the Outway will use the authorization service
config.authorizationService.url(empty)URL of the authorization service
config.authorizationService.caCertificatePEMExistingSecret.name(empty)The name of the secret containing the certificate
config.authorizationService.caCertificatePEMExistingSecret.keyca.crtThe key in the secret that contains the certificate
config.authorizationService.withBodyfalseWhen true the request to the authorization server will contain the body of the original request
config.authorizationService.maxBodySize4096The maximum request body size in bytes that is allowed. If a body exceeds this limit, the request will fail
config.authorizationService.maxChunkSize1024The chunk size in bytes that is used to process each request body chunk

Docker compose

Below you see a Docker Compose setup that has OPA configured as authorization server, only the relevant environment values are included.

Note that AUTHORIZATION_SERVICE_ADDRESS is the full URL of the decision rule, not the base URL of OPA. For the policy above that is /v1/data/httpapi/outway/allow.

This guide expects root-ca.pem to be present in the folder ./certs

services:
outway:
image: docker.io/federatedserviceconnectivity/outway:latest
volumes:
- ./certs:/certs:ro
environment:
AUTHORIZATION_SERVICE_ADDRESS: https://opa-service.example.com/v1/data/httpapi/outway/allow
AUTHORIZATION_ROOT_CA: /certs/root-ca.pem

Relevant environment values

Environment variableDefaultDescription
AUTHORIZATION_SERVICE_ADDRESS(empty)The address of the authorization server
AUTHORIZATION_ROOT_CA(empty)Path to a PEM-encoded certificate, only needed when the authorization server provides TLS

Verifying

  • Replace <outway-address> with the address of your Outway
  • Replace <grant-hash> with a grant-hash of a ServiceConnectionGrant of a valid Contract

Calling the Outway without the Authorization header should fail.

curl <outway-address> -H 'Fsc-Grant-Hash: <grant-hash>'

The response should have an HTTP status 401 with the message authorization service denied request

Calling the Outway with the Authorization header should succeed.

curl <outway-address> -H 'Fsc-Grant-Hash: <grant-hash>' -H 'Authorization: very-secret-api-key'

This should return a response from the API exposed through FSC.