This is an experimental project for encrypting content on Balena IoT devices (such as the Raspberry Pi) to avoid your content being accessed when devices are lost or stolen. Content can include both folders/files and environment variables, such as encrypted API keys stored in your Balena Cloud.
The difficultly with implementing this functionality on devices like the Raspberry Pi, is your device must be given a password to decrypt content. If you keep that password on the device, it is no longer secure. If you have to go around and enter the password on all your devices, you remove the ability to manage devices remotely. Secure Store aims to overcome this by serving the secure key (your password to unlock the device) from a Store Server device, which the Store Client devices must be able to see and access to decrypt the content stored on them. Example use cases include:
If you identify other use cases please do let us know so we can explore further iterations.
This repo is set up as a demo of Secure Store and can be deployed using the Deploy to Balena button below.
Instead of using two separate devices, it uses two containers merely to demonstrate the decryption process in action. A two container setup as demonstrated here would provide no security benefits as the keys are all stored on the same device, it is purely for demonstration processes. To build between devices as it is designed for, see the Setting up your own
section below.
When your containers start, you will see the Client looking for the Server container. When it finds it, it will decrypt the demo content and environment variables using the key it has fetched from the Server. You will then be able to go in to the container and see both the encrypted and decrypted content, as well as see a basic shell script in your terminal/logs that is started after decryption and is accessing the decrypted content.
The encryption only applies to environment variables and the files and folders you specify, not the whole operating system, which avoids the performance impact that can come from full disk encryption.
mTLS certificates use SHA256WithRSA and encrypted environment variables use AES-GCM with a 32bit key for AES-256. The code for generating both these components is available in this repository for your own auditing and development.
The files and folders are encrypted using Rclone Crypt, which is also open source and available for your own auditing. Under the hood are different encryption methods for different parts of the encryption (e.g. filenames vs files themselves) which utilise among others NaCl SecretBox based on XSalsa20 cipher and Poly1305 for integrity. It's content is encrypted using a randomly generated 1024 bit password which is unique for each device, and stored inside a configuration file. That configuration file is then encrypted with NaCl SecretBox using your own provided key generated from Secure Store and served by Secure Store Server to all your devices.
As with any encryption solution, this is not bullet proof. This project has been developed as a proof of concept designed to significantly increase the level of security of content, but does not make any guarantees.
There are many different workflows for setting up and running this project for yourself. You should avoid copying unencrypted content into your containers and doing any sort of encryption in the Docker build steps as an attacker could potentially extract the unencrypted content from your Docker layers. mTLS keys and user keys should ALWAYS be stored in GitHub Secrets or other secure means, and not in GitHub repos. For the purposes of demonstrating the setup however, we will keep the keys in the repo for ease of understanding.
:grey_exclamation: Tip: If you are only looking to use the environment variable decryption (without any data storage) pass the -env-only
flag and you can skip steps 2, 3 and 5.
./encrypted
and ./keys
./source
with the files you want to store encryptedsecure-store-server
with the hostname of the device as it will be seen by the Client. For example, secure-store.local
or https://mystore.com
.docker run ghcr.io/balena-labs-research/secure-store -generate-keys -hostname secure-store-server -base64
You will see two mTLS keys outputted to your terminal as base64 strings. Add these to your Balena Cloud account as environment variables MTLS_CERT
and MTLS_KEY
.
If you would rather store the mTLS keys as files on the device, remove the -base64
flag and add -v ${PWD}/keys:/app/keys -certificate-path keys/cert.pem -key-path keys/key.pem
to generate files for inclusion in the root directory of the Secure Store executable.
my-password-eQ4al9jgPxlWDwxL6uiGdznhhVJzaVQPnkNRjwvwoTvqWpeBJJJZ
and is included below, replace it with your own.docker run \
--device /dev/fuse \
--cap-add SYS_ADMIN \
-v ${PWD}/source:/app/source \
-v ${PWD}/encrypted:/app/storage \
-v ${PWD}/keys:/app/keys \
ghcr.io/balena-labs-research/secure-store \
-encrypt-content ./source/. \
-config-path ./keys/encrypt.conf \
-password my-password-eQ4al9jgPxlWDwxL6uiGdznhhVJzaVQPnkNRjwvwoTvqWpeBJJJZ
The files generated in ./keys
should be stored in your GitHub Secrets and written to your container on build rather than kept in your GitHub repo but for now we will continue by copying them in to the containers through the Dockerfiles to be more transparent on how it works.
-string
is the variable value to encrypt, and -password
is the same password as chosen above for encrypting files.docker run ghcr.io/balena-labs-research/secure-store -password my-password-eQ4al9jgPxlWDwxL6uiGdznhhVJzaVQPnkNRjwvwoTvqWpeBJJJZ -string this-is-my-new-test-api-key
Secure Store will print an encrypted version of your passed string. We include these in our Balena Dashboard as environment variables to be decrypted later. Add them in to your dashboard in the following format:
ENCRYPTED_TESTVAR=d72afca84fb28581a2d9d533835a41cacb4beef534797825da7d8b80889593540077b2a4d9abb6db260108541903b6ce44a1a9
The ENCRYPTED_
prefix tells Secure Store to handle these environment variables. Once the environment variable has been decrypted this prefix will be stripped and your environment variable in your container will become: TESTVAR=this-is-my-test-api-key
STORE_PASSWORD
. For example: STORE_PASSWORD=my-password-eQ4al9jgPxlWDwxL6uiGdznhhVJzaVQPnkNRjwvwoTvqWpeBJJJZ
. Below is an example of a Balena Cloud Dashboard setup:You are now ready to start your devices. Enjoy!
You can start the client in local mode by passing -local your-password
. This will create the mount and not rely on the server. Your password could however, be extracted from your device if you keep the two together without additional security measures. You should consider compiling the password used, or the means of generating the password into the Golang binary.
When the device decrypts the content, it will create a folder called decrypted
on the path specified (defaults to ./
). In this folder you can write new content and it will be included in your encrypted storage. The decrypted
folder is a mount, that points to the encrypted content behind it stored in a folder called storage
. If you make storage
a volume, then the encrypted content will be persistent.