Inject Nextcloud secrets via environment variables

Much of what is automated today tends to be done through environment variables. Although this does not necessarily make the system more secure, because environment variables under Linux are nothing more than a file in a different location, attack vectors, such as automated attacks that look for passwords in certain (known) places, are at least made more difficult this way.

In addition, on a system where you are working with another service provider, for example, you do not necessarily want them to be able to read the database password directly.

Preliminary note

Nextcloud is OpenSource and therefore offers the possibility to have a closer look into the source code.

The Config.php can also be read via environment variables, if they start with the prefix "NC_".

The Config.php can also be read via environment variables, if they start with the prefix "NC_".

* Returns a config value
*
* gets its value from an `NC_` prefixed environment variable
* if it doesn't exist from config.php
* if this doesn't exist either, it will return the given `$default`
*
* @param string $key key
* @param mixed $default = null default value
* @return mixed the value or $default
*/

public function getValue($key, $default = null) {
$envKey = self::ENV_PREFIX . $key;
if (isset($this->envCache[$envKey])) {
return $this->envCache[$envKey];
}

if (isset($this->cache[$key])) {
return $this->cache[$key];
}

return $default;
}
/nextcloud/lib/private/Config.php


The developers always give preference to environment variables: If, for example, the environment variable "NC_data" exists, it overwrites the value of data in Nextcloud config.php.

Protect secrets securely with HashiCorps Vault

When it comes to passwords, HashiCorps Vault is widely used.
The learning curve is high. However, you don't have to understand everything to use it.

A brief outline:
Vault uses a client server architecture.
The server stores the encrypted database.
To access this database, the Vault client must unseal the Vault.

This requires three unsealing keys and the root password. Since the server does not store the root token, it is not possible to open the Vault with a pure root token after losing the unsealing keys.

Install vault server:
Create a new directory vault with three subfolders:

mkdir -p volumes/{config,file,logs}

Create a configuration file vault.json in the Docker volumes directory with the following content:

{
  "backend": {
    "file": {
      "path": "/vault/file"
    }
  },
  "listener": {
    "tcp":{
      "address": "0.0.0.0:8200",
      "tls_disable": 1
    }
  },
  "ui": true
}
vault.json

And last but not least a docker compose file:

version: '3.1'
services:
  vault:
    image: vault
    container_name: vault
    ports:
      - "127.0.0.1:8200:8200"
    restart: always
    volumes:
    - ./volumes/logs:/vault/logs
      - ./volumes/file:/vault/file
      - ./volumes/config:/vault/config
      cap_add:
      - IPC_LOCK
      entrypoint: vault server -config=/vault/config/vault.json
docker-compose.yml

Start with:

docker-compose up -d

Vault client
Install the Vault Client depending on the operating system:

Install | Vault | HashiCorp Developer
Explore Vault product documentation, tutorials, and examples.

Initialize vault

export VAULT_ADDR='http://127.0.0.1:8200'
vault operator init -key-shares=6 -key-threshold=3

You will retrieve 6  Unseal keys and 1 Root Token. Be sure to keep unsealing keys and root tokens! You will need them after every login or reboot!

Now you have to use any three Unseal Keys for unsealing:

vault operator unseal *KeyNo1*
vault operator unseal *KeyNo2*
vault operator unseal *KeyNo3*

After successful unsealing, you can log in with the root token:

vault login **Root Token**

And initialize the database:

vault secrets enable -version=2 -path=secret kv

You can save secrets to the database using the following method:

vault kv put secret/app/password password=123

You can read passwords via:

vault kv get -field=password secret/app/password

Passwords are always versioned in Vault. So you can track every change (or accidental deletion).

For the Nextcloud passwords we use the path NC. The command would be accordingly:

vault kv put NC/NC_database password=supersafepassword

In my example I did this for NC_dbname, NC_dbuser and NC_dbpassword. So I would like to erase the database name, database user and database password from the Nextcloud configuration file and replace them with environment variables.

Adapt NGINX

NGINX does not support environment variables out of the box.
We use the detour with the tool "envsubst", which is always included with NGINX.
Our placeholders are set via fastcgi_param directive.

But first, let's take it one step at a time.

Envsubst
envsubst can be configured via a template file.
You create the template file simply by copying your actual nextcloud NGINX configuration file.

We give the copy the extension .template.
For example, if your file in /etc/nginx/sites-availabe/ is called nextcloud.conf, the copy is called nextcloud.conf.template.

If you followed the official NGINX guide from Nextcloud, you have the following configuration block:

    location ~ \.php(?:$|/) {
            ...
     }

Set the placeholders for the environment variables above the line "fastcgi_pass php-handler;"

    location ~ \.php(?:$|/) {
         ...
         fastcgi_param front_controller_active true;
         fastcgi_param NC_dbname '${NC_dbname}';
         fastcgi_param NC_dbuser '${NC_dbuser}';
         fastcgi_param NC_dbpassword '${NC_dbpassword}';
         ...
     }
nextcloud.conf.template

You can test the whole thing. Export e.g. NC_database with a test value:

export NC_dbname=TEST

And substitute envsubst for a test:

envsubst '{$NC_dbname}' < nextcloud.conf.template > test.conf

The newly created test.conf file should now have an entry for fastcgi_param NC_dbname:

    location ~ \.php(?:$|/) {
         ...
         fastcgi_param front_controller_active true;
         fastcgi_param NC_dbname 'TEST';
         fastcgi_param NC_dbuser '${NC_dbuser}';
         fastcgi_param NC_dbpassword '${NC_dbpassword}';
         ...
     }
test.conf

If everything worked out that way, you're done. Later we will not write to a test file, but to the actual Nginx Nextcloud configuration. Don't forget to delete test.conf, because you might get errors.

Write the secrets from the Vault to a file

Now we need to establish the connection from the Vault to Nextcloud.
First we write a small script to write the Vault secrets for Nextcloud into a file "NC_environment.conf:

#!/bin/bash
# Populate Nextcloud environment variables with Hashicorp vault secrets
# NC_dbname
# NC_dbuser
# NC_dbpassword

for NCkey in NC_dbname NC_dbuser NC_dbpassword
do
    echo "creating $NCkey"
    NCkeyTemp=$NCkey
    NCkeyTemp="$(vault kv get -field=password NC/$NCkey)"
    echo "$NCkey=$NCkeyTemp" >> /tmp/NC_environment.conf
    
done

# Restart NGINX
systemctl restart nginx

# RM environment Fiel
rm /tmp/NC_environment.conf

#EOF
createEnvironment.sh

Make this file executable.

chmod +x createEnvironment.sh

This script writes all keys from your Vault as readable environment variables to the file NC_environment.conf.
The file is read by the NGINX daemon and deleted after restarting NGINX.

You can already run the script as a test, but it won't do anything yet because we still need to make a change to the NGINX systemd configuration.

Modify the unit file of NGINX

We are almost ready. However, the transfer of the environment variables to NGINX is missing.

Edit the unit file from NGINX:

systemctl edit nginx.service

And add the following lines:

[Service]
ExecStartPost=/bin/sleep 0.1
EnvironmentFile=/tmp/NC_environment.conf
ExecStartPre=/usr/bin/bash -c '/usr/bin/envsubst \'$${NC_dbname} $${NC_dbuser} $${NC_dbpassword}\' < /etc/nginx/conf.d/nextcloud.conf.template > /etc/nginx/conf.d/nextcloud.conf & /usr/sbin/nginx -t -q -g "daemon on; master_process on;"'
ExecStart=/usr/bin/bash -c 'usr/sbin/nginx -g "daemon on; master_process on;" && /usr/bin/sed -i "/NC_/d" /etc/nginx/conf.d/nextcloud.conf'

### Lines below this comment will be discarded

Adjust the main file again. You have to exclude the lines ExecStart and ExeStartPre, otherwise we will get an error:

systemctl edit --full nginx.service
[Service]
User=www-data
Group=www-data
Type=forking
#ExecStartPre=/usr/bin/bash -c "usr/bin/env && /usr/sbin/nginx -t -q -g 'daemon on; master_process on;'"
#ExecStart=/usr/sbin/nginx -g 'daemon on; master_process on;'
ExecReload=/usr/sbin/nginx -g 'daemon on; master_process on;'-s reload

Now you can remove the entries dbname, dbuser and dbpassword from your Nextcloud configuration in the Nextcloud configuration directory and start the script createEnvironment.sh.

Here is what happens:
The script reads the secrets from HashiCorp's Vault and writes them to the /tmp/NC_environment.conf file.
After that Nginx is restarted.
Nginx gets the environment variables from the Vault through the unit file: EnvironmentFile=/tmp/NC_environment.conf
The environment variables are written to NGINX's nextcloud.conf file via envsubst and applied by your Nextcloud instance.
After the changes are applied, the entry is no longer needed.
Therefore, the sed command in the ExecStart line removes each fastcgi_param line with NC_.

Some considerations

The environment variables are passed to each NGINX process. It is therefore possible that unauthorized processes can also have access to the secrets.

Starting from version 250 of Systemd there is the possibility to create encrypted credentials. The feature is called systemd-creds.
Since Debian 11 still runs with Sytemd 247, I could not test this.

The Nextcloud OC (Cli commands) need the entries in the Nextcloud configuration file and cannot do anything with the environment variables.
Who often works with the Nextcloud CLI, should thank that or write a wrapper.