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:
| Value | Default | Description |
|---|---|---|
https.enabled | false | enables 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.keyCertificate | tls.crt | The key in the secret that contains the certificate |
https.existingSecret.keyPrivateKey | tls.key | The 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(namedhttp) becomes8443(namedhttps). LISTEN_ADDRESSbecomes0.0.0.0:8443andLISTEN_HTTPSbecomes1.- The Secret is mounted read-only at
/certificate-server, andTLS_SERVER_CERT/TLS_SERVER_KEYpoint into it. - The Service exposes
service.httpsPort(default443) namedhttpsinstead ofservice.httpPortnamedhttp.
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 variable | Default | Description |
|---|---|---|
LISTEN_HTTPS | false | enables 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_ADDRESS | 127.0.0.1:8080 | Listen 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
| Value | Default | Description |
|---|---|---|
config.authorizationService.enabled | false | When 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.key | ca.crt | The key in the secret that contains the certificate |
config.authorizationService.withBody | false | When true the request to the authorization server will contain the body of the original request |
config.authorizationService.maxBodySize | 4096 | The maximum request body size in bytes that is allowed. If a body exceeds this limit, the request will fail |
config.authorizationService.maxChunkSize | 1024 | The 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 variable | Default | Description |
|---|---|---|
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 agrant-hashof 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.