---
name: azion-configure-mtls-on-a-workload
description: >-
  Upload a Trusted CA certificate and a CRL, turn on mTLS on a workload, and test the handshake, with the Azion CLI or the API.
---

# Configure mTLS on a workload

You can turn on mutual TLS ([mTLS](/en/documentation/platform/workloads/mtls/)) on a [workload](/en/documentation/platform/workloads/) with the [Azion CLI](/en/documentation/devtools/cli/) or the API. To serve HTTPS on your domain with a server certificate of your own instead, refer to [Upload a digital certificate](/en/documentation/guides/application-security/tls-and-certificates/digital-certificates/).

With mTLS on, the workload asks each client for a certificate during the TLS handshake. It checks that certificate against a Trusted CA certificate: the certificate of the certificate authority (CA) that signs your clients' certificates. The setup takes three objects: the Trusted CA certificate, an optional certificate revocation list (CRL) from the same CA, and the `mtls` object of the workload.

An account that runs on API v3 with Domains sets mTLS on each domain instead. For more information, refer to [Domains](/en/documentation/platform/workloads/domains/).

---

Select an interface. The prerequisites and the steps of each task follow your choice.

## Prerequisites

- mTLS activated on your account. Azion activates mTLS per account, so contact the Azion Sales team to turn it on.
- A workload whose deployment names an application, served over HTTPS. The client certificate travels in the TLS handshake, so mTLS checks HTTPS connections only. To create a workload, refer to [Workloads quickstart](/en/documentation/platform/workloads/quickstart/).
- The certificate of your CA, in PEM format. A third-party CA issues it. A certificate that Azion generates cannot serve as the Trusted CA.
- (Optional) A CRL in PEM format, signed by the same CA.
- A client certificate that CA signed, with its private key, to test the workload.

**CLI**

- The [Azion CLI](/en/documentation/devtools/cli/), authorized with your account. This page matches Azion CLI 4.23.0.
- The workload ID. `azion create workload` prints it as `Created Workload with ID <workload-id>`.

**API**

- A personal token for the `Authorization` header, in the form `Token [TOKEN VALUE]`. To create a token, refer to [Personal tokens](/en/documentation/fundamentals/personal-tokens/).
- `curl` or another HTTP client.
- The workload ID. `azion create workload` prints it as `Created Workload with ID <workload-id>`.

---

## Upload the Trusted CA certificate

[Certificate Manager](/en/documentation/platform/workloads/#certificate-manager) stores the Trusted CA certificate with the type `trusted_ca_certificate` and no private key. The file must be in PEM format, with its `-----BEGIN CERTIFICATE-----` and `-----END CERTIFICATE-----` lines, and Certificate Manager refuses any other format. When your chain has intermediate certificates, include them in the same file.

**CLI**

To upload the certificate with the Azion CLI, pass the PEM file of your CA, here `ca.pem`:

```bash
azion create digital-certificate --name my-trusted-ca --certificate ca.pem --certificate-type trusted_ca_certificate
```

The command prints the ID of the new certificate. The workload update in Turn on mTLS on the workload needs it:

```text
Created Digital Certificate with ID <trusted-ca-id>
```

**API**

To upload the certificate with the API, send a `POST` request to the certificates endpoint. Write the certificate as one string, with each line break as `\n`:

```bash
curl --request POST \
  --url https://api.azion.com/v4/workspace/tls/certificates \
  --header 'Accept: application/json' \
  --header 'Authorization: Token [TOKEN VALUE]' \
  --header 'Content-Type: application/json' \
  --data '{
  "name": "my-trusted-ca",
  "type": "trusted_ca_certificate",
  "certificate": "-----BEGIN CERTIFICATE-----\n<certificate-body>\n-----END CERTIFICATE-----\n"
}'
```

The API answers `201` with the new certificate. Keep its `id` for the workload update.

The Trusted CA certificate reads `inactive` in Certificate Manager until a workload names it in its `mtls` object.

---

## Upload a certificate revocation list

A CRL lists the certificates that a CA revoked before their expiration date, and the CA signs it. A workload with mTLS on names up to 100 CRLs in `mtls.config.crl`. This task is optional: skip it when your CA publishes no CRL.

**CLI**

To upload the CRL with the Azion CLI, pass its PEM file, here `ca.crl`, and the name of the CA that issued it:

```bash
azion create crl --name my-crl --issuer "My CA" --crl ca.crl
```

The command prints the ID of the new CRL:

```text
Created Certificate Revocation List with ID <crl-id>
```

**API**

To upload the CRL with the API, send a `POST` request to the CRLs endpoint. Write the CRL as one string, with each line break as `\n`:

```bash
curl --request POST \
  --url https://api.azion.com/v4/workspace/tls/crls \
  --header 'Accept: application/json' \
  --header 'Authorization: Token [TOKEN VALUE]' \
  --header 'Content-Type: application/json' \
  --data '{
  "name": "my-crl",
  "issuer": "My CA",
  "crl": "-----BEGIN X509 CRL-----\n<crl-body>\n-----END X509 CRL-----\n"
}'
```

The API answers `201` with the new CRL. Keep its `id` for the workload update.

Certificate Manager stores the CRL and reads its `last_update` and `next_update` dates from the CRL itself.

---

## Turn on mTLS on the workload

The `mtls` object of the workload turns the check on and names the Trusted CA certificate, the CRLs, and the verification mode. Pick the mode in `verification`:

- `enforce`: the workload ends the handshake when a client presents no certificate, or a certificate the Trusted CA did not sign.
- `permissive`: every client completes the handshake, and a firewall rule decides which requests to refuse.

For what each mode does with each kind of client, refer to [Verification modes](/en/documentation/platform/workloads/mtls/#verification-modes).

**CLI**

To turn on mTLS with the Azion CLI, save a JSON file with the workload ID and the `mtls` object, here as `mtls.json`. Send `null` in `crl` when the workload names no CRL:

```json
{
  "id": <workload-id>,
  "mtls": {
    "enabled": true,
    "config": { "certificate": <trusted-ca-id>, "crl": [<crl-id>], "verification": "enforce" }
  }
}
```

Update the workload with the file:

```bash
azion update workload --file mtls.json
```

The command prints the ID of the workload it updated:

```text
Updated Workload with ID <workload-id>
```

To confirm the change, describe the workload:

```bash
azion describe workload --workload-id <workload-id> --format json
```

This excerpt of the output shows the `mtls` object as the workload stores it:

```json
{
 "mtls": { "config": { "certificate": <trusted-ca-id>, "crl": [<crl-id>], "verification": "enforce" }, "enabled": true }
}
```

**API**

To turn on mTLS with the API, send a `PATCH` request to the workload with the `mtls` object. Send `null` in `crl` when the workload names no CRL:

```bash
curl --request PATCH \
  --url https://api.azion.com/v4/workspace/workloads/<workload-id> \
  --header 'Accept: application/json' \
  --header 'Authorization: Token [TOKEN VALUE]' \
  --header 'Content-Type: application/json' \
  --data '{
  "mtls": {
    "enabled": true,
    "config": { "certificate": <trusted-ca-id>, "crl": [<crl-id>], "verification": "enforce" }
  }
}'
```

The API accepts the update.

The Trusted CA certificate now reads `active` in Certificate Manager. The new mode takes several minutes to reach all of Azion's distributed infrastructure. Until then, some requests still meet the previous configuration, so repeat a test before you trust its result.

The update fails when a certificate ID sits in the wrong field. A server certificate in `mtls.config.certificate` is refused with `Invalid certificate type, MUST be a Trusted CA.` A Trusted CA certificate in `tls.certificate` is refused with `Invalid certificate type, MUST be an Edge Certificate.` The CLI prints the message inside `Error: Failed to update the Workload: [...]`. For both fixes, refer to [mTLS errors](/en/documentation/platform/workloads/mtls/#errors).

---

## Test the handshake

Two requests to a domain of the workload show whether it checks clients: one without a client certificate, and one with a certificate the Trusted CA signed. Send them after the mTLS change propagates.

To send a request with no client certificate, run `curl` in verbose mode:

```bash
curl -skv https://<your-domain>/ -o /dev/null
```

In `enforce` mode, the workload requests a certificate, the client sends none, and the handshake fails. `curl` exits with code `56`. With `curl` built on LibreSSL, the verbose output ends like this:

```text
* (304) (IN), TLS handshake, Request CERT (13):
* (304) (IN), TLS handshake, Certificate (11):
* (304) (IN), TLS handshake, CERT verify (15):
* (304) (IN), TLS handshake, Finished (20):
* (304) (OUT), TLS handshake, Certificate (11):
* (304) (OUT), TLS handshake, Finished (20):
* SSL connection using TLSv1.3 / AEAD-AES256-GCM-SHA384 / [blank] / UNDEF
* LibreSSL SSL_read: LibreSSL/3.3.6: error:1404C45C:SSL routines:ST_OK:reason(1116), errno 0
```

To send a request with the client certificate, pass the certificate and its private key, here `client.pem` and `client.key`:

```bash
curl -sk --cert client.pem --key client.key https://<your-domain>/ -o /dev/null -w '%{http_code}\n'
```

The handshake completes, and `curl` prints the status code your application returns:

```text
200
```

The same results hold on the workload domain, `<id>.map.azionedge.net`. In `permissive` mode, both requests complete the handshake and reach the application, and so does a request with a certificate another CA signed.

---

## Deny requests without a valid client certificate

In `permissive` mode, the workload refuses no client, so a rule in [Rules Engine for Firewall](/en/documentation/platform/firewall/rules-engine/) must refuse the requests whose client certificate failed the check. The rule acts only when the firewall is in the workload's deployment. To bind one, refer to [Bind a firewall to a workload](/en/documentation/guides/application-security/firewall-and-waf/firewall-protect-your-domain/).

This rule denies the requests to `<your-domain>` whose client certificate did not pass validation:

```json
{
  "name": "deny-unverified-clients",
  "active": true,
  "criteria": [
    [
      { "variable": "${host}", "conditional": "if", "operator": "is_equal", "argument": "<your-domain>" },
      { "variable": "${client_certificate_validation}", "conditional": "and", "operator": "is_not_equal", "argument": "true" }
    ]
  ],
  "behaviors": [{ "type": "deny" }]
}
```

In Azion Console, the same rule uses the variables *Host* and *Client Certificate Validation*, the operators *is equal* and *is not equal*, and the behavior *Deny (403 Forbidden)*. To create the rule from Azion Console, the CLI, or the API, refer to [Rules Engine for Firewall](/en/documentation/platform/firewall/rules-engine/).

Once the rule propagates, a request to `<your-domain>` without a valid client certificate receives `403 Forbidden`. A client whose certificate the Trusted CA signed still reaches the application.

---

## Pass client certificate details to the origin

The Open Banking model requires the origin to receive the client certificate in request headers, from the variables `${ssl_client_escaped_cert}` and `${ssl_client_s_dn_parsed}`. `${ssl_client_escaped_cert}` holds the client certificate as a URL-encoded PEM string, and `${ssl_client_s_dn_parsed}` holds its subject Common Name (CN). A Request Phase rule in [Rules Engine for Applications](/en/documentation/platform/applications/rules-engine/) adds each header with the `add_request_header` behavior, on the application in the workload's deployment.

This rule adds the client certificate to every request that carries one, in the `Escaped-Client-Cert` header:

```json
{
  "name": "mtls-client-certificate",
  "active": true,
  "criteria": [[{ "variable": "${ssl_client_escaped_cert}", "conditional": "if", "operator": "exists", "argument": "" }]],
  "behaviors": [{ "type": "add_request_header", "attributes": { "value": "Escaped-Client-Cert: ${ssl_client_escaped_cert}" } }]
}
```

The header argument takes the form `Header-Name: value`, and Azion Console refuses any other shape with `Header must follow the header-name: value format`. Add a second rule of the same shape for `${ssl_client_s_dn_parsed}`, under a header name of your choice. To create the rules from Azion Console, the CLI, or the API, refer to [Rules Engine for Applications](/en/documentation/platform/applications/rules-engine/). The other client certificate variables are listed in [mTLS variables](/en/documentation/platform/applications/rules-engine/#mutual-transport-layer-security-mtls-variables).

Once the rules propagate, each request that Azion sends to the origin for a client with a certificate carries the headers.

---

## Next steps

- [mTLS](/en/documentation/platform/workloads/mtls.md): Look up every field of the mtls object, what each verification mode does, and the errors.
- [Bind a firewall to a workload](/en/documentation/guides/application-security/firewall-and-waf/firewall-protect-your-domain.md): Put a firewall in the workload's deployment, so its rules can deny unverified clients.
- [Rules Engine for Applications](/en/documentation/platform/applications/rules-engine.md): Find every client certificate variable a rule can read and pass to the origin.
- [Troubleshoot Workloads](/en/documentation/platform/workloads/troubleshooting.md): Find why a certificate is refused or a TLS handshake fails.
