6

The Strapi framework (as far as I understand) requires the database password to be provided at launch. Usually, the password is specified in the database.js file, like this:

module.exports = ({ env }) => ({
  defaultConnection: 'default',
  connections: {
    default: {
      connector: 'bookshelf',
      settings: {
        client: 'postgres',
        host: '/cloudsql/myDatabaseInstanceName',
        database: 'databaseName',
        username: 'databaseUsername',
        password: 'databasePassword',
      },
    },
  },
});

This of course is not very secure, as the database.js file is usually committed to the repo.

Therefore, some people inject the password into the database.js file, instead storing it as an environment variable:

module.exports = ({ env }) => ({
  defaultConnection: 'default',
  connections: {
    default: {
      connector: 'bookshelf',
      settings: {
        client: 'postgres',
        host: `/cloudsql/${env('INSTANCE_CONNECTION_NAME')}`,
        database: env('DATABASE_NAME'),
        username: env('DATABASE_USERNAME'),
        password: env('DATABASE_PASSWORD'),
      },
    },
  },
});

However, this is also not very secure. In many runtime environments (including Google App Engine, which I am using) the environment passwords can be viewed, in plaintext, by any project user.

Ideally, I would like to store the database password in a secret vault (I'm using Google Secret Manager), and somehow provide the password from the vault to the database.js file at launch. But I don't understand how to implement that? Is it even possible to access a secret vault from database.js? Or, how else might I securly inject my database password into Strapi?

Thanks!

Mathew Alden
  • 1,458
  • 2
  • 15
  • 34

3 Answers3

1
  • @raxetul comment is the easiest way. A google secret can be accessed as environment variable from the code. Per Google - "is common, but should be avoided when possible" here
  • To retrieve the secret by async code before the initial db configuration you should use the Strapi bootstrap function.

The bootstrap function is called at every server start. You can use it to add a specific logic at this moment of your server's lifecycle.

  • Whatever you choose - remember that anyone who can change the code - has access to the password, no matter how secure you store it. A "rogue" developer can add a line like console.log(env('HIGHLY_ENCRYPTED_PASSWORD')) and read the output.
yeya
  • 1,968
  • 1
  • 21
  • 31
  • `bootstrap.js` needs to return some sort of JS object, correct? Is the object in the same format as the object exported by `database.js`? – Mathew Alden May 25 '21 at 20:29
  • Doesn't have to return anything, can return a promise that store the secret somewhere you can reference from database.js. – yeya May 26 '21 at 04:01
  • Wait, how do I "store the secret somewhere you can reference from database.js"? I tried just using a global variable, but apparently those don't work the way I thought they did. Then I tried exporting a global variable, but that's not allowed. – Mathew Alden May 28 '21 at 22:26
1

Solution for Strapi V4 - After trying so many solutions I finally found one. We have to do it in index.js > "async register" function. I tried in bootstrap.js as well but it didnt work.

//index.js
    ....
    ....
    async register({ strapi }) {
    try {
      console.log("Fetching database secrets....");
      const secretData = await getSecret();
      console.log("Database secrets fetched successfully !!!");
      strapi.config.set("database.connection.connection.host", secretData.host);
      strapi.config.set("database.connection.connection.database", secretData.dbname);
      strapi.config.set("database.connection.connection.user", secretData.username);
      strapi.config.set("database.connection.connection.password", secretData.password);
      strapi.config.set("database.connection.connection.port", secretData.port);

    } catch (err) {
      console.log("error is secret fetch call.", err)
    }

  },
 ....
 ....

And simply is database.js try to connect with other default configurations.

//database.js
module.exports = ({ env }) => {
  return {
    connection: {
      client: 'postgres',
      connection: {
        //postgres connection is set in index.js, no need to set it here again.
        schema: env('DATABASE_SCHEMA', 'public'), // Not required

        ssl: {
          rejectUnauthorized: env.bool('DATABASE_SSL_SELF', false), // For self-signed certificates
        },
        useNullAsDefault: true,
      }
    }
  }
};
Amit Bhoyar
  • 1,007
  • 5
  • 20
0

Use dotenv and dotenv-defaults packages.

For default and noncritical values use ".env.defaults" file and commit this file into your VCS. This will provide a one-click environment setup for other developers.

If your team members want to override values, they should use gitignored ".env" file in their local development environments. This will prevent mistaken commits.

In your server, define environmental variables externally and use them without embedding them into your code. This will provide security to your live production environment.

As in the documents of these packages, you can use any value from these files or environmental variables as like

....,
password: process.env.MY_PASSWORD,
....
raxetul
  • 439
  • 5
  • 12
  • 1
    Thanks for your reply! This is definitely one approach, but (as I vaguely mentioned in my question) I was hoping to find a solution *more* secure than storing the sensitive data in the prod environment variables, because the environment variables can be seen by everyone who has access to the Google Cloud Platform project. – Mathew Alden May 19 '21 at 19:51
  • You should use Secret Manager which passes encrypted stored passwords into your application environment. So in the final, your application read secrets from environmental variables but those environmental variables come from a secure place. https://cloud.google.com/run/docs/configuring/secrets – raxetul May 19 '21 at 20:07
  • So I'd like to use Secret Manager, but the API call to get a secret is async, and I can't figure out how to call an async function before exporting the config object. Maybe I can use a top-level await? But those don't work in JS files, only in ES modules... I don't have a lot of experience with JS; it's unclear to me *how* to use Secret Manager. – Mathew Alden May 20 '21 at 13:58