---
title: Configure your mobile app or IoT device · Cloudflare SSL/TLS docs
description: This tutorial demonstrates how to configure your Internet-of-things
  (IoT) device and mobile application to use client certificates with API
  Shield.
lastUpdated: 2025-10-28T21:24:57.000Z
chatbotDeprioritize: false
source_url:
  html: https://developers.cloudflare.com/ssl/client-certificates/configure-your-mobile-app-or-iot-device/
  md: https://developers.cloudflare.com/ssl/client-certificates/configure-your-mobile-app-or-iot-device/index.md
---

This tutorial demonstrates how to configure your Internet-of-things (IoT) device and mobile application to use client certificates with [API Shield](https://developers.cloudflare.com/api-shield/).

## Scenario details

This walkthrough uses the example of a device that captures temperature readings and transmits them by sending a POST request to a Cloudflare-protected API. A mobile application built in Swift for iOS retrieves those readings and displays them.

To keep this example simple, the API is implemented as a Cloudflare Worker (borrowing code from the [To-Do List tutorial on building a jamstack app](https://developers.cloudflare.com/workers/tutorials/build-a-jamstack-app/)).

Temperatures are stored in [Workers KV](https://developers.cloudflare.com/kv/concepts/how-kv-works/) using the source IP address as a key, but you can easily use a [value from the client certificate](https://developers.cloudflare.com/cloudflare-one/access-controls/service-credentials/mutual-tls-authentication/), such as the fingerprint.

The example API code below saves a temperature and timestamp into KV when a POST is made and returns the most recent five temperatures when a GET request is made.

```js
const defaultData = { temperatures: [] };


const getCache = (key) => TEMPERATURES.get(key);
const setCache = (key, data) => TEMPERATURES.put(key, data);


async function addTemperature(request) {
  // Pull previously recorded temperatures for this client.
  const ip = request.headers.get("CF-Connecting-IP");
  const cacheKey = `data-${ip}`;
  let data;
  const cache = await getCache(cacheKey);
  if (!cache) {
    await setCache(cacheKey, JSON.stringify(defaultData));
    data = defaultData;
  } else {
    data = JSON.parse(cache);
  }


  // Append the recorded temperatures with the submitted reading (assuming it has both temperature and a timestamp).
  try {
    const body = await request.text();
    const val = JSON.parse(body);


    if (val.temperature && val.time) {
      data.temperatures.push(val);
      await setCache(cacheKey, JSON.stringify(data));
      return new Response("", { status: 201 });
    } else {
      return new Response(
        "Unable to parse temperature and/or timestamp from JSON POST body",
        { status: 400 },
      );
    }
  } catch (err) {
    return new Response(err, { status: 500 });
  }
}


function compareTimestamps(a, b) {
  return -1 * (Date.parse(a.time) - Date.parse(b.time));
}


// Return the 5 most recent temperature measurements.
async function getTemperatures(request) {
  const ip = request.headers.get("CF-Connecting-IP");
  const cacheKey = `data-${ip}`;


  const cache = await getCache(cacheKey);
  if (!cache) {
    return new Response(JSON.stringify(defaultData), {
      status: 200,
      headers: { "content-type": "application/json" },
    });
  } else {
    data = JSON.parse(cache);
    const retval = JSON.stringify(
      data.temperatures.sort(compareTimestamps).splice(0, 5),
    );
    return new Response(retval, {
      status: 200,
      headers: { "content-type": "application/json" },
    });
  }
}


export default {
  async fetch(request, env, ctx) {
    return request.method === "POST"
      ? addTemperature(request)
      : getTemperatures(request);
  },
};
```

***

## 1. Validate API

### POST sample data to API

To validate the API before adding mTLS authentication, POST a random temperature reading:

```sh
$ TEMPERATURE=$(echo $((361 + RANDOM %11)) | awk '{printf("%.2f",$1/10.0)}')
$ TIMESTAMP=$(date -u +"%Y-%m-%dT%H:%M:%SZ")


$ echo -e "$TEMPERATURE\n$TIMESTAMP"
36.70
2020-09-28T02:54:56Z


$ curl --verbose --header "Content-Type: application/json" --data '{"temperature":'''$TEMPERATURE''', "time": "'''$TIMESTAMP'''"}' https://shield.upinatoms.com/temps 2>&1 | grep "< HTTP/2"
< HTTP/2 201
```

### GET sample data from API

A GET request to the `temps` endpoint returns the most recent readings, including the one submitted in the example above:

```sh
$ curl --silent https://shield.upinatoms.com/temps | jq .
[
  {
    "temperature": 36.3,
    "time": "2020-09-28T02:57:49Z"
  },
  {
    "temperature": 36.7,
    "time": "2020-09-28T02:54:56Z"
  },
  {
    "temperature": 36.2,
    "time": "2020-09-28T02:33:08Z"
  }
]
```

***

## 2. Create Cloudflare-issued certificates

Before you can use API Shield to protect your API or web application, create Cloudflare-issued client certificates.

You can [create a client certificate in the Cloudflare dashboard](https://developers.cloudflare.com/ssl/client-certificates/create-a-client-certificate/).

However, since most developers working at scale generate their own private keys and certificate signing requests via API, this example uses the Cloudflare API to create client certificates.

To create a bootstrap certificate for the iOS application and the IoT device, this example uses [Cloudflare’s public key infrastructure toolkit, CFSSL](https://github.com/cloudflare/cfssl):

```bash
# Generate a private key and CSR for the iOS device.


$ cat <<'EOF' | tee -a csr.json
{
    "hosts": [
        "ios-bootstrap.devices.upinatoms.com"
    ],
    "CN": "ios-bootstrap.devices.upinatoms.com",
    "key": {
        "algo": "rsa",
        "size": 2048
    },
    "names": [{
        "C": "US",
        "L": "Austin",
        "O": "Temperature Testers, Inc.",
        "OU": "Tech Operations",
        "ST": "Texas"
    }]
}
EOF


$ cfssl genkey csr.json | cfssljson -bare certificate


2020/09/27 21:28:46 [INFO] generate received request
2020/09/27 21:28:46 [INFO] received CSR
2020/09/27 21:28:46 [INFO] generating key: rsa-2048
2020/09/27 21:28:47 [INFO] encoded CSR


$ mv certificate-key.pem ios-key.pem
$ mv certificate.csr ios.csr


# Do the same for the IoT sensor.


$ sed -i.bak 's/ios-bootstrap/sensor-001/g' csr.json
$ cfssl genkey csr.json | cfssljson -bare certificate
...
$ mv certificate-key.pem sensor-key.pem
$ mv certificate.csr sensor.csr


# now ask that these CSRs be signed by the private CA issued for your zone
# we need to replace actual newlines in the CSR with ‘\n’ before POST’ing
$ CSR=$(cat ios.csr | perl -pe 's/\n/\\n/g')
$ request_body=$(< <(cat <<EOF
{
  "validity_days": 3650,
  "csr":"$CSR"
}
EOF
))


# save the response so we can view it and then extra the certificate
$ curl https://api.cloudflare.com/client/v4/zones/{zone_id}/client_certificates \
--header "X-Auth-Email: <EMAIL>" \
--header "X-Auth-Key: <API_KEY>" \
--header "Content-Type: application/json" \
--data "$request_body" > response.json


$ cat response.json | jq .


{
  "success": true,
  "errors": [],
  "messages": [],
  "result": {
    "id": "7bf7f70c-7600-42e1-81c4-e4c0da9aa515",
    "certificate_authority": {
      "id": "8f5606d9-5133-4e53-b062-a2e5da51be5e",
      "name": "Cloudflare Managed CA for account 11cbe197c050c9e422aaa103cfe30ed8"
    },
    "certificate": "-----BEGIN CERTIFICATE-----\nMIIEkzCCA...\n-----END CERTIFICATE-----\n",
    "csr": "-----BEGIN CERTIFICATE REQUEST-----\nMIIDITCCA...\n-----END CERTIFICATE REQUEST-----\n",
    "ski": "eb2a48a19802a705c0e8a39489a71bd586638fdf",
    "serial_number": "133270673305904147240315902291726509220894288063",
    "signature": "SHA256WithRSA",
    "common_name": "ios-bootstrap.devices.upinatoms.com",
    "organization": "Temperature Testers, Inc.",
    "organizational_unit": "Tech Operations",
    "country": "US",
    "state": "Texas",
    "location": "Austin",
    "expires_on": "2030-09-26T02:41:00Z",
    "issued_on": "2020-09-28T02:41:00Z",
    "fingerprint_sha256": "84b045d498f53a59bef53358441a3957de81261211fc9b6d46b0bf5880bdaf25",
    "validity_days": 3650
  }
}


$ cat response.json | jq .result.certificate | perl -npe 's/\\n/\n/g; s/"//g' > ios.pem


# Now ask that the second client certificate signing request be signed.


$ CSR=$(cat sensor.csr | perl -pe 's/\n/\\n/g')
$ request_body=$(< <(cat <<EOF
{
  "validity_days": 3650,
  "csr":"$CSR"
}
EOF
))


$ curl https://api.cloudflare.com/client/v4/zones/{zone_id}/client_certificates \
--header "X-Auth-Email: <EMAIL>" \
--header "X-Auth-Key: <API_KEY>" \
--header "Content-Type: application/json" \
--data "$request_body" | perl -npe 's/\\n/\n/g; s/"//g' > sensor.pem
```

***

## 3. Embed the client certificate in your mobile app

To configure the mobile app to securely request temperature data submitted by the IoT device, embed the client certificate in the mobile app.

For simplicity, this example embeds a “bootstrap” certificate and key in the application bundle as a PKCS#12-formatted file:

```sh
$ openssl pkcs12 -export -out bootstrap-cert.pfx -inkey ios-key.pem -in ios.pem
Enter Export Password:
Verifying - Enter Export Password:
```

In a real-world deployment, a bootstrap certificate should only be used in conjunction with users' credentials to authenticate with an API endpoint that can return a unique user certificate. Corporate users will want to use mobile device management (MDM) to distribute certificates.

### Embed the client certificate in an Android app

The following is an example of how you may use a client certificate in an Android app to make HTTP calls. You need to add the following permission in `AndroidManifest.xml` to allow an Internet connection.

```xml
<uses-permission android:name="android.permission.INTERNET" />
```

For demonstration purposes, the certificate in this example is stored in `app/src/main/res/raw/cert.pem` and the private key is stored in `app/src/main/res/raw/key.pem`. You may also store these files in other secure manners.

The following example uses an `OkHttpClient`, but you may also use other clients such as `HttpURLConnection` in similar ways. The key is to use the `SSLSocketFactory`.

```java
private OkHttpClient setUpClient() {
    try {
        final String SECRET = "secret"; // You may also store this String somewhere more secure.
        CertificateFactory certificateFactory = CertificateFactory.getInstance("X.509");


        // Get private key
        InputStream privateKeyInputStream = getResources().openRawResource(R.raw.key);
        byte[] privateKeyByteArray = new byte[privateKeyInputStream.available()];
        privateKeyInputStream.read(privateKeyByteArray);


        String privateKeyContent = new String(privateKeyByteArray, Charset.defaultCharset())
                .replace("-----BEGIN PRIVATE KEY-----", "")
                .replaceAll(System.lineSeparator(), "")
                .replace("-----END PRIVATE KEY-----", "");


        byte[] rawPrivateKeyByteArray = Base64.getDecoder().decode(privateKeyContent);
        KeyFactory keyFactory = KeyFactory.getInstance("RSA");
        PKCS8EncodedKeySpec keySpec = new PKCS8EncodedKeySpec(rawPrivateKeyByteArray);


        // Get certificate
        InputStream certificateInputStream = getResources().openRawResource(R.raw.cert);
        Certificate certificate = certificateFactory.generateCertificate(certificateInputStream);


        // Set up KeyStore
        KeyStore keyStore = KeyStore.getInstance(KeyStore.getDefaultType());
        keyStore.load(null, SECRET.toCharArray());
        keyStore.setKeyEntry("client", keyFactory.generatePrivate(keySpec), SECRET.toCharArray(), new Certificate[]{certificate});
        certificateInputStream.close();


        // Set up Trust Managers
        TrustManagerFactory trustManagerFactory = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm());
        trustManagerFactory.init((KeyStore) null);
        TrustManager[] trustManagers = trustManagerFactory.getTrustManagers();


        // Set up Key Managers
        KeyManagerFactory keyManagerFactory = KeyManagerFactory.getInstance(KeyManagerFactory.getDefaultAlgorithm());
        keyManagerFactory.init(keyStore, SECRET.toCharArray());
        KeyManager[] keyManagers = keyManagerFactory.getKeyManagers();


        // Obtain SSL Socket Factory
        SSLContext sslContext = SSLContext.getInstance("TLS");
        sslContext.init(keyManagers, trustManagers, new SecureRandom());
        SSLSocketFactory sslSocketFactory = sslContext.getSocketFactory();


        // Finally, return the client, which will then be used to make HTTP calls.
        OkHttpClient client = new OkHttpClient.Builder()
                .sslSocketFactory(sslSocketFactory, (X509TrustManager) trustManagers[0])
                .build();


        return client;


    } catch (CertificateException | IOException | NoSuchAlgorithmException | KeyStoreException | UnrecoverableKeyException | KeyManagementException | InvalidKeySpecException e) {
        e.printStackTrace();
        return null;
    }
}
```

The above function returns an `OkHttpClient` embedded with the client certificate. You can now use this client to make HTTP requests to your API endpoint protected with mTLS.

***

## 4. Embed the client certificate on your IoT device

To prepare the IoT device for secure communication with the API endpoint, embed the certificate on the device and configure the device to use the certificate when making POST requests.

This example assumes the certificate and the private key are securely copied to `/etc/ssl/private/sensor-key.pem` and `/etc/ssl/certs/sensor.pem`.

The sample script is modified to point to these files:

```python
import requests
import json
from datetime import datetime


def readSensor():


    # Takes a reading from a temperature sensor and store it to temp_measurement


    dateTimeObj = datetime.now()
    timestampStr = dateTimeObj.strftime('%Y-%m-%dT%H:%M:%SZ')


    measurement = {'temperature':str(temp_measurement),'time':timestampStr}
    return measurement


def main():


    print("Cloudflare API Shield [IoT device demonstration]")


    temperature = readSensor()
    payload = json.dumps(temperature)


    url = 'https://shield.upinatoms.com/temps'
    json_headers = {'Content-Type': 'application/json'}
    cert_file = ('/etc/ssl/certs/sensor.pem', '/etc/ssl/private/sensor-key.pem')


    r = requests.post(url, headers = json_headers, data = payload, cert = cert_file)


    print("Request body: ", r.request.body)
    print("Response status code: %d" % r.status_code)
```

When the script attempts to connect to `https://shield.upinatoms.com/temps`, Cloudflare requests that a client certificate is sent and the script sends the contents of `/etc/ssl/certs/sensor.pem`. Then, as required to complete the SSL/TLS handshake, the script demonstrates it has possession of `/etc/ssl/private/sensor-key.pem`.

Without the client certificate, the Cloudflare rejects the request:

```txt
Cloudflare API Shield [IoT device demonstration]
Request body:  {"temperature": "36.5", "time": "2020-09-28T15:52:19Z"}
Response status code: 403
```

When the IoT device presents a valid client certificate, the POST request succeeds and the temperature reading is recorded:

```txt
Cloudflare API Shield [IoT device demonstration]
Request body:  {"temperature": "36.5", "time": "2020-09-28T15:56:45Z"}
Response status code: 201
```

***

## 5. Enable mTLS

After creating Cloudflare-issued certificates, the next step is to [enable mTLS](https://developers.cloudflare.com/ssl/client-certificates/enable-mtls/) for the hosts you want to protect with API Shield.

***

## 6. Configure API Shield to require client certificates

To configure API Shield to require client certificates, [create a mTLS rule](https://developers.cloudflare.com/api-shield/security/mtls/configure/#create-an-mtls-rule).
