3

Background:

I'm running Craft Pro 3.1.19 on PHP 7.2.15

Craft CMS, by default, uses vlucas/phpdotenv to get the configuration (containing secrets / credentials and lots of other stuff) from .env

Craft CMS logs PHP errors to storage/logs/phperrors.log

In the PHP error log, the all the data from .env is logged.

The data is available in the $_SERVER superglobal, and is appended to stack traces.

Example:

Error: Call to undefined function this_is_a_fatal_error() in /srv/app/web/index.php:24
Stack trace:
#0 {main}
...
...
$_SERVER = [
    ...
    ...
    'SECURITY_KEY' => 'exposed-secret'
    'DB_USER' => 'my-db-user',
    'DB_PASSWORD' => 'exposed-secret',
    'DB_DATABASE' => 'my-db-name',
    ...
]

In the Craft CMS web.log, sensitive data is masked before writing the log. Yay!

Example:

Stack trace:
#0 {main}
...
...
$_SERVER = [
    ...
    ...
    'SECURITY_KEY' => '**************'
    'DB_USER' => 'my-db-user',
    'DB_PASSWORD' => '**************',
    'DB_DATABASE' => 'my-db-name',
    ...
]

Problem:

How do I prevent secrets beeing logged in the PHP error log?

I know this is not a Craft CMS specific problem.

The secrets could ofcourse also be exposed in other ways than in the log, with a careless phpinfo() or something similar.

So, I've searched a lot, and read a lot, and found various options / suggestions.

I'd like to ask for your opinion on the options / solutions (listed below), or if you have any more options I've not found yet...

Some of the stuff I've read:

http://movingfast.io/articles/environment-variables-considered-harmful/

https://blog.fortrabbit.com/how-to-keep-a-secret

https://nystudio107.com/blog/multi-environment-config-for-craft-cms

https://12factor.net/config

(I have more, but I don't have the rep to post more links)

A general conclusion I've made so far is that configuration in the environment is a good idea, but credentials / secrets in the environment is a bad idea. Is this a general consensus?

Option 1 - turn off error logging:

This has been brought up before; https://github.com/craftcms/cms/issues/3619 - https://docs.craftcms.com/v3/config/php-constants.html#craft-log-php-errors

So, the solution here is to tell Craft to turn off PHP error logging (you also have to turn of logging in php.ini).

I really do not want to turn of error logging unless all the other options are not possible...

Option 2 - Move production configration to the webserver:

The phpdotenv documentation used to say "phpdotenv is made for development environments, and generally should not be used in production."

https://github.com/vlucas/phpdotenv/issues/207

Even though the documentation does not say this anymore, it might still be a good idea to move the config from .env to the webserver config (in production).

For Nginx - you can add a fastcgi_param for each key/value to the vhost config.

You can use a tool like nystudio107/dotenvy to help getting the correct config.

This does not really solve anything, as the secrets are still loaded into the environment.

Bonus: This has a small performance benefit.

Option 3 - Modify the way Craft CMS uses phpdotenv:

Laravel also uses vlucas/phpdotenv, but not in the same way. It reads the .env, and stores the config in a cache file.

This way, the environment is not "polluted" with sensitive data. See https://laravel.com/docs/5.8/configuration#configuration-caching

Could this be a viable option for Craft CMS as well? Would probably need to add some more steps when deploying code?

Sounds great to me, but may break backwards compatibility. Also, this requires P&T to do some magic.

Option 4 - Roll your own?

Remove vlucas/phpdotenv, and use something else to encrypt the .env-file, or implement option 3 myself.

I've found these (all are on github, but I don't have the reputation to post enough complete links):

  • arrilot/dotenv-php
  • johnathanmiller/secure-env-php
  • psecio/secure_dotenv
  • josegonzalez/php-dotenv

Has anyone done something like this? The environment is still "polluted", but it won't be in plain text when it's logged.

This is good enough for me, but "roll your own security/crypto" isn't the best idea...

Option 5 - Something I do not know about yet?

Do you know something I don't? Please tell me! Do you have any suggestions for further reading?

Thanks!

Update / clarification:

To give a bit more context, that I left out of the original post to try to keep it a bit more simple:

  • I'm writing a lot about "I" and "me", but I'm part of a bigger team of developers and ops-people from a provider.
  • We're using AWS CloudWatch, and all our logs are sent to this service.
  • We're following a set of company imposed security guidelines that requires us to evaluate the storage and "containment" of secrets.

I'm not really afraid of the people having access to the server. If they have access, they most likley know about, and can access the secrets in .env directly anyway.

I'm more concerned about a rouge debug-statment in our code, a bug in Craft / Yii / a plugin or something like that, that exposes the secrects inadvertently. So option 1, turning off logging, doesn't really solve the problem either.

More people have access to the AWS dashboards / CloudWatch, also the ops-people that are on rotation in the evening/nights, who may or may not have in depth knowledge about our application, or what data not to send around.

It would also be nice to have some sort of dashboard on a giant screen of our own in the office, showing the status and latest errors on the servers, without having to worry about random guests or colleagues from other departments seeing our precious credentials.

If this was my own personal server setup, with just me as a user/developer, I'd not think of this whole thing as a problem.

  • The first question I'd ask is... does it matter? Who can access the webserver in such a way that they can read the PHP logs? And if they have access to that, they can probably view any non-encrypted file that the webserver can view. – andrew.welch Mar 30 at 1:42
  • I've added an update with more info about the context @andrew.welch – Knut Erik Apr 1 at 7:56
  • Did you ever figure out a way to fix this? This problem is not related to dotenv at all, I'm having the same problem when running it containerized in AWS Fargate since the only way to configure the setup is via ENV variables. Dumping them blindly without sanitation on every error seems utterly stupid. Maybe the best way would be to run the same sanitizer for stderr as for stdout, but I can't figure out how. – jishi Sep 27 at 8:46
1

First, full disclosure here, the company I work for is a customer of Fort Rabbit. The secrets.json approach is something I think that strikes the balance between flexibility and security for really sensitive keys like API, OAuth, Amazon S3 and other sensitive secrets. dotenv is great, but having these in plain text is a risk, which I think you are mindful of.

The approach I take is to use the secrets.json concept. With Craft 3, you can make your secrets available as environment variables, but they won't be visible when dumping out phpinfo().

secrets.json

{
  "CUSTOM": {
      "SECRET_KEY_1": "secretValue",
      "SECRET_KEY_2": "anotherSecretValue"
      ....
  }
}

config/app.php

// Create secrets as env variables
if (getenv('APP_SECRETS')) {

    $secrets = json_decode(file_get_contents(getenv('APP_SECRETS')), true);

    if (isset($secrets['CUSTOM'])) {
        foreach($secrets['CUSTOM'] as $k => $v) {
            putenv("$k=$v");
        }
    }
}

.env

APP_SECRETS="/app/secrets.json"

You set the APP_SECRETS environment variable which will be the file path to your JSON file.

You can then reference the values in secrets.json as environment variables with getenv() and even the Craft $SECRET_KEY_1 syntax if a plugin supports Craft::parseEnv on a field/setting.

This is however a bit bespoke and tailored to Fort Rabbit's environment, but the concept can be applied generally.

There is also the whole other argument about dotenv in production due to the overhead and performance impact, but that's another story!

I'm not sure there is a perfect solution, but I hope that gives you one example or what I'd consider "good practice".

  • I'm not sure I understand how this helps anything? It looks like just one more indirection, but ultimately the $_ENV variables are still set (otherwise getenv() wouldn't work). – andrew.welch Mar 30 at 1:40
  • @andrew.welch In this case, if you were to dump out the contents of phpinfo() or the $_SERVER $_ENV globals, they wouldn't show up, given the way they are initialised. In the Fort Rabbit environment secrets are encrypted in their dashboard and everywhere in the system, except during runtime. Not saying it's perfect, but it's better than plain .env. – James White Mar 30 at 7:17
  • Thanks for the suggestion James, but I must be missing something. I just tried this, and all of the content in secrets.json was available when checking the ENV section of phpinfo(), together with the stuff som .env. – Knut Erik Apr 4 at 9:37
  • @KnutErik Interesting. When I do phpinfo() on my local environment (Nanobox). I do not see them at all, so might be a false sense of security. I haven't tried having phpinfo() dumped on Fort Rabbit, but maybe they do show up. I think the clarity would be Fort Rabbit's approach probably doesn't answer your question, but the storage of them is a bit more secure, so they aren't just exposed in plain text within their platform. – James White Apr 4 at 9:48

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.