Introduction
In this example, we will describe how to create two Python Flask applications that run securely within an AWS Nitro enclave and can communicate over a secure connection using TLS. Each application will first receive a TLS certificate signed by the Fortanix Confidential Computing Manager (CCM). The corresponding private key is generated from within the enclave and is never exposed outside the enclave. The TLS connection itself is terminated within the enclave, thereby achieving end-to-end security.
Create Client/Server Applications
Let us start by creating the two applications.
Python Server Application
Create a file named server.py
containing the following code:
import ssl
import uuid
from flask import jsonify, Flask
from flask_api import status
app = Flask("Python Flask TLS Server")
@app.route('/random')
def get_random_token():
random_token = uuid.uuid4().hex[:6]
return jsonify({
"token": random_token
}), status.HTTP_200_OK
if __name__ == "__main__":
context = ssl.SSLContext(ssl.PROTOCOL_TLSv1_2)
context.verify_mode = ssl.CERT_REQUIRED
context.load_verify_locations("/app/cert/ca.crt")
context.load_cert_chain("/app/cert/server.crt", "/app/cert/server.key")
app.run(host="0.0.0.0", port=2222, use_reloader=False, threaded=True, ssl_context=context)
This is a very simple server listening on port 2222
, accepts GET requests at path /random
, and returns a random token. The server is configured to use the HTTPS protocol using a certificate at location "/app/cert/server.crt
", with its corresponding key at "/app/cert/server.key
". The certificate of the Certificate Authority that signed the server.crt
certificate is located at "/app/cert/ca.crt
". The line context.verify_mode = ssl.CERT_REQUIRED
indicates that the sender of requests (in this case the client) needs to be verified using its certificate, thereby establishing a mutual TLS connection.
When this application is deployed using Enclave OS, Enclave OS will first generate the certificate key and store it at the location "/app/cert/server.key
" within the enclave. It will then submit a Certificate Signing Request (CSR) to Fortanix CCM and receive back a signed certificate which will be stored at "/app/cert/server.crt
".
Next, let us turn this application into a docker container. First, create a file called dockerfile.server
with the following contents:
FROM python:3.7
RUN mkdir /app
RUN mkdir /app/cert
COPY server.py /app/
RUN pip3 install flask Flask-API
WORKDIR /app
ENTRYPOINT ["python3", "server.py"]
Then build the container and push it to a registry using the commands:
docker build -t fortanix/python-tls-server -f dockerfile.server .
docker push fortanix/python-tls-server
In the above commands, you can adjust the tag to point to your own container registry, or simply skip this step (you do not have permission to push to the Fortanix docker hub registry) since this container is already pushed to the Fortanix public registry (https://hub.docker.com/r/fortanix/python-tls-server).
Python Client Application
Similar to the server application above, the client application will receive a signed certificate from Fortanix CCM. In response to receiving requests from a user, the client application uses this certificate to establish a mutual TLS connection with the server and submit a request to the server. The server response, which is a random token in our simple example, will be sent back to the user.
Create a file called client.py
containing the following code:
import os
import requests
import ssl
from flask import Flask
SERVER_DOMAIN = "flask-server.domain"
ENDPOINT = "https://" + SERVER_DOMAIN + ":2222"
app = Flask("Python Flask TLS Client")
@app.route("/submit")
def submit_request():
path = ENDPOINT + "/random"
req = requests.get(url=path, verify='/app/cert/ca.crt', cert=("/app/cert/client.crt", "/app/cert/client.key"))
return req.content
if __name__ == '__main__':
os.system("echo '0.0.0.0 " + SERVER_DOMAIN + "' >> /etc/hosts")
context = ssl.SSLContext(ssl.PROTOCOL_TLSv1_2)
context.load_verify_locations("/app/cert/ca.crt")
context.load_cert_chain("/app/cert/client.crt", "/app/cert/client.key")
app.run(host="0.0.0.0", port=3333, use_reloader=False, threaded=True, ssl_context=context)
Next, create a docker file named dockerfile.client
with these contents:
FROM python:3.7
RUN mkdir /app
RUN mkdir /app/cert
COPY client.py /app/
RUN pip3 install flask Requests
WORKDIR /app
ENTRYPOINT ["python3", "client.py"]
Build and push the client container using the commands:
docker build -t fortanix/python-tls-client -f dockerfile.client .
docker push fortanix/python-tls-client
The client container is already available here: https://hub.docker.com/r/fortanix/python-tls-client.
Create Fortanix CCM applications
Now that we have the containers of both applications available to a registry, let us create Fortanix CCM applications for both the client and the server, so we can convert the containers into ones that run within enclaves, using Fortanix Enclave OS.
Follow the same steps described in this example to issue REST API request to:
Authenticate to Fortanix CCM.
Select an account.
Recall that session tokens are short-lived and you will occasionally have to issue a refresh API request whenever you get
{"message": "Forbidden", "code": "FORBIDDEN"}.
When you reach the step for creating an application, use the following server.json
(replace the output_image_name
with your private registry) file shown below with the API request:
curl -b $cpath -c $cpath -H "X-CSRF-Header:true" -H "Content-Type: application/json" -d @server.json -X POST https://ccm.fortanix.com/v1/apps
{
"name": "Python TLS Server",
"description": "A python server using Flask and a CCM certificate",
"input_image_name": "fortanix/python-tls-server",
"output_image_name": "fortanix-private/python-tls-server-nitro",
"default_build_settings": {
"sgx": {},
"nitro_enclaves": {
"cpu_count": 2,
"mem_size": 1024,
"enable_overlay_filesystem_persistence": true
}
},
"group_id": "a8e8395e-096d-4eb8-9017-2098f2ab8327",
"allowed_domains": [
"flask-server.domain"
],
"advanced_settings": {
"entrypoint": [],
"manifestEnv": [],
"encryptedDirs": [],
"certificate": {
"issuer": "MANAGER_CA",
"subject": "flask-server.domain",
"keyType": "RSA",
"keyParam": {
"size": 2048
},
"keyPath": "/app/cert/server.key",
"certPath": "/app/cert/server.crt"
},
"caCertificate": {
"caPath": "/app/cert/ca.crt",
"system": "false"
},
"rw_dirs": [
"/app/cert"
]
},
"custom_metadata": {
"app_type": "ENCLAVE_OS"
}
}
Make a note of the <app_id>
and the (pending_task_id
) in the response since we will be using these in the next two requests.
{
"name":"Python TLS Server",
"app_id":"1ef7d352-28b6-4560-a714-a503435455bc",
"pending_task_id":"d5550480-697e-4782-9bc5-58aaaa97e712",
"domains_added":["flask-server.domain"],
...
}
Since this application contains a certificate with a domain, you will need to whitelist the application domain:
curl -b $cpath -c $cpath -H "X-CSRF-Header:true" -H "Content-Type: application/json" -d '{"status":"APPROVED"}' -X PATCH https://ccm.fortanix.com/v1/tasks/<task_id>
Finally, follow the steps in this example to create and whitelist an image. You will need to use the <app_id>
as part of these steps.
Follow the exact same steps to create a Fortanix CCM application for the client application, as well as create and whitelist an image. Use the client.json
file when creating your client application:
{
"name": "Python TLS Client",
"description": "A python client using Flask and a CCM certificate",
"input_image_name": "fortanix/python-tls-client",
"output_image_name": "fortanix-private/python-tls-client-nitro",
"default_build_settings": {
"sgx": {},
"nitro_enclaves": {
"cpu_count": 2,
"mem_size": 1024,
"enable_overlay_filesystem_persistence": true
}
},
"group_id": "a8e8395e-096d-4eb8-9017-2098f2ab8327",
"allowed_domains": [
"flask-client.domain"
],
"advanced_settings": {
"entrypoint": [],
"manifestEnv": [],
"encryptedDirs": [],
"certificate": {
"issuer": "MANAGER_CA",
"subject": "flask-client.domain",
"keyType": "RSA",
"keyParam": {
"size": 2048
},
"keyPath": "/app/cert/client.key",
"certPath": "/app/cert/client.crt"
},
"caCertificate": {
"caPath": "/app/cert/ca.crt",
"system": "false"
},
"rw_dirs": [
"/app/cert"
]
},
"custom_metadata": {
"app_type": "ENCLAVE_OS"
}
}
Run the converted applications
At this point, we have everything we need to deploy and test our converted applications on a compute node that has been enrolled in CCM that will run securely within an enclave.
Let us start by running the server application using the following command, replacing the <ip_addr> with the IP address of your compute node and the image name to your image in your private registry:
docker run --privileged --volume /dev:/dev -v /run/nitro_enclaves:/run/nitro_enclaves -e NODE_AGENT_BASE_URL=http://<ip_addr>:9092/v1 -p 2222:2222 fortanix-private/python-tls-server-nitro:latest
On the same compute node, run the client application on a separate terminal window using:
docker run --privileged --volume /dev:/dev -v /run/nitro_enclaves:/run/nitro_enclaves -e NODE_AGENT_BASE_URL=http://:9092/v1 -p 3333:3333 --network host fortanix-private/python-tls-client-nitro:latest
Now that both the server and the client are running, we can use yet another terminal window to submit a request using curl:
curl -k --cacert ca.pem https://localhost:3333/submit
ca.pem
is the certificate of the authority that signed the server.crt
and client.crt
certificates delivered to the applications. The ca.pem
file is also called the Zone Certificate and it is specific to your Fortanix CCM account. All applications under the same account receive certificates signed by the same Zone certificate.
To get the CA certificate, you can use the attached script: fetch_ca.sh
Conclusion
In this example, we have created two applications that run securely within an enclave. Both applications receive TLS certificates from the Fortanix Confidential Computing Manager which they then use to establish secure channels through which they can communicate with each other but also with external users.