Deployment

To use the Active API Armour, both endpoints, producer and consumer, need to deploy it locally in their private networks.

Architecture

If you have a particularly large topology, you might want to install the API Armour on its own server. This decision depends on the specification of your machines and how you want to spread the load between them. Currently horizontal scaling is in the development stage and will be available within next releases.

_images/architecture.jpg

Consider running this software in isolated compute environments, such as an AWS Nitro Enclave instance, to further protect and securely process highly sensitive data computed by the software.

Multiprocessing

Re:lock’s Active API Armour is by default ready for vertical scalability. You can achieve this by enabling multi-process functionality through the addition of the “–multiprocessing” parameter to the initialization command. Once activated, the system will efficiently utilize all available CPUs within the system.

docker run --privileged --network host -it relock/armour run \
       --host 127.0.0.1 --ip 127.0.0.1 --port 443 \
       --multiprocessing

Vertical scaling is achieved by launching multiple workers inside the container. No additional reverse proxy or extra software deployment is required for this functionality.

Minimal example

This runs a completely in-memory enclave server, which is useful for development but should not be used in production:

docker pull relock/armour
docker run --privileged --network host -it relock/armour run \
           --host 127.0.0.1 --port 8111 --multiprocessing

We recommend running this container in isolated compute environments, such as an AWS Nitro Enclave instance, to further protect and securely process highly sensitive data computed by the software.

You can install python package in the usual way using pip:

pip install relock

Once installed, a minimal consumer request may look like this:

from relock import TCP as Armour

http = requests.Session()
armour = Armour(host='127.0.0.1',
                port=8111,
                name='Alice')

if response := http.get('http://' + host,
                        headers={'Content-Type': 'application/json',
                                  **arm.headers()},
                        json={}):
    if ticket := arm.stamp(response.headers):
        logging.info(response.json())

More detailed examples of the producer and consumer application implementations can be found in our GitHub repository.

Minimal producer server

We will use Flask and Python as an example of a REST API producer. There are several ways we can go about building APIs with Flask, for example RESTful, but in this particular example we will use pure Flask and minimal dependencies.

As a detailed Flask description is not a subject of this documentation, we are not explaining typical ‘how to start’ or python environment deployment. Refer to Flask abundant public documentation in case of any doubts.

Implementation of the Armour with a Flask server is, in fact, an implementation of two functions. One function is ticket validation before the request is processed and the other function is a simple redirect pass of the required headers to the response.

app = Flask(__name__, instance_relative_config=True)
armour = Armour(host='127.0.0.1',
                port=8111,
                private=True,
                pool=5)

@app.before_request
@Timer.timer
def validate():
    with armour() as arm:
        if not arm.validate(request.path, request.headers, request.remote_addr):
            return Response('Unauthorized', status=401)

As you can see, ticket validation on the producer side is straightforward. It requires an additional request to the enclave and passing of the headers sent by the consumer. We assume that we validate the ticket only if X-Ticket-ID is passed, leaving room for other identity providers and headers’ processing.

If the ticket is valid and the response is properly processed, we should add the required return ticket headers using the @after_request decorator.

@app.after_request
def after_request(response):
    with armour() as arm:
        arm.finalize(response)

Active API Armour does not require any additional routes and in the ‘registration’ phase will request by default ‘/’ root route to exchange the initial secret between the consumer and the producer. If needed, the ‘registration’ may be implemented in different ways; nonetheless, the consumer side will need to be informed about it and have their Armour modified accordingly.

Request processing and access to the ephemeral encryption keys is automated and does not require any additional steps. You can access the key using the ‘ticket’ attribute of the request object.

@app.route("/")
def index():
    with armour() as arm:
        if request.json.get('time') if request.is_json else None:
            logging.info('Decrypted %s', arm.decrypt(request.json.get('time')))
        return {"time": arm.encrypt(time.time())}

Note that from the producer perspective, you have access to two ephemeral keys at the same time. You use the prior encryption key to decrypt the request payload sent by the consumer, and the new ephemeral AEAD key to encrypt your payload response.

Example data class that automates this flow and allows you to encrypt/decrypt the payload with AES-GCM-128 using implemented default ticket methods is included in the python library. For the library that simplifies the enclave requesting and ticket processing, take a look at our GitHub repository.

Minimal consumer

Integration of the API Armour on the consumer side is divided in two stages. First, we need to implement the registration (pairing with the producer) and then we can start to make requests.

In the production-ready version of the system there will be ticket bundles and bundle management processes implemented that automate the registration phase. In the current development version these processes are not available yet, which means that every container in the system must be manually served during the initial registration.

In the development version the registration ticket should be delivered by the producer in a secondary channel, such as e-mail or another trusted way. For registration, first save the ticket in the consumer enclave, identifying it with the name of the producer’s service.

if aport == 443:
    from app.client.http import HTTP as Armour
else:
    from app.client.tcp import TCP as Armour


http = requests.Session()

armour = Armour(host=armour,
                port=aport,
                name=name,
                pool=1)

with armour(ticket, host, port) as arm:
    """ Request part
    """

Once the ticket is ready to use, armour will “introduce” itselves to the producer autmatically. This operation will generate the shared root secret and create the entangled mutual identity between enclaves, thus confirming their relation.

while True:
    if response := http.get('http://' + host,
                            headers={'Content-Type': 'application/json',
                                      **arm.headers()},
                            json={'time': arm.encrypt(time.time())}):
        if ticket := arm.stamp(response.headers):
            logging.info('Decrypted %s', arm.decrypt(response.json().get('time')))

From this moment the use of the API Armour is autonomous and there is no more management of keys needed.

Exact process identification (ticket requesting PID) by the local enclave is ensured by the ticket chaining mechanism. Every time when we request for a new ticket, the immediately preceding ticket is required. This mechanism is an important part of intruder detection and fail-safe disconnect in case of an impersonation attempt.

This configuration also implies that the local system should possess only as many tickets in use as the legitimate application services running. Detection of any illegitimate access to the ticket system is, therefore, radically simplified. Example request to producer with encryption is very easy:

if response := http.get('http://' + host,
                        headers={'Content-Type': 'application/json',
                                  **arm.headers()},
                        json={'time': arm.encrypt(time.time())}):
    if ticket := arm.stamp(response.headers):
        logging.info('Decrypted %s', arm.decrypt(response.json().get('time')))

Stamp and token functions are local enclave requests. Token is an ask for a new ticket and stamp validates the response, and returns the corresponding new ticket with the appropriate encryption key.

A complete functional example, including additional libraries which simplify the data flow and operations, may be found in our GitHub repository.

Shutdown

Generally, a graceful shutdown is preferable in the case of any system that saves its state. When the standard shutdown procedures are not done with Armour, the result can be data corruption of program and operating records. The result of the corruption can be instability, incorrect functioning or failure of entangled identities.

Maintenance

One of the standout features of the Active API Armour is the elimination of any need for key management, enabled by the use of ephemeral keys and continuous re-keying of the root secret. No tasks are required to maintain a zero trust security posture.

Keep the enclave running and isolated - it’s as simple as that.