Confused about file permissions for master.key

Thin dies in production because the www-data user has no access to the config/master.key (well, duh):

2024-01-23 19:46:20 +0000 Writing PID to tmp/pids/
2024-01-23 19:46:20 +0000 Changing process privilege to www-data:www-data
2024-01-23 19:46:20 +0000 Using rack adapter
2024-01-23 19:46:27 +0000 Exiting!
/var/www/myapp/vendor/bundle/ruby/3.1.0/gems/activesupport-7.1.2/lib/active_support/encrypted_file.rb:122:in `binread': Permission denied @ rb_sysopen - /var/www/myapp/config/master.key (Errno::EACCES)

I have tried feeding Thin the key via the RAILS_MASTER_KEY environment variable:

RAILS_MASTER_KEY=`cat config/master.key` thin start -C config/thin_production.yml

But this doesn’t seem to make the slightest difference. Extensive trawling has failed to turn up anything relevant, which makes me think it must be something simple - but what?

Just to reassure myself I have the right end of the stick I tried changing ownership of master.key to www-data (don’t worry, my server is not publicly accessible), and yep, now the app server works. So it’s as I thought: Thin needs to be handed the master key somehow. I’m surprised this isn’t documented anywhere, must be a common problem. How do I set the RAILS_MASTER_KEY environment variable for Thin?

I think I’ve solved it. Since the init.d script runs as root it is able to read the master key before spawning Thin, and now passing it as an environment variable works - though I’m not sure why this wasn’t working from the command line. In any case, my init.d/thin now looks like this:


# Exit if the package is not installed
[ -x "$DAEMON" ] || exit 0

  [ -e "$CONFIG_PATH/$2.yml" ] && CONFIGS=$CONFIG_PATH/$2.yml
  for conf in $CONFIGS
    echo "[$1] $conf"
    user=`grep "^user: " $conf|sed -e 's/^user: //g'`
    group=`grep "^group: " $conf|sed -e 's/^group: //g'`

    # Switch to the app's directory and set permissions
    cd `grep "^chdir: " $conf|sed -e 's/^chdir: //g'`
    [ -d ./log ] && chown $user:$group ./log
    [ -d ./tmp ] && chown -R $user:$group ./tmp

    # Read the master key
    MASTER_KEY=`cat config/master.key` 

    # Run with bundler
    if [ -e Gemfile ] && grep thin Gemfile > /dev/null; then
      RAILS_MASTER_KEY=$MASTER_KEY $BUNDLE exec $DAEMON $1 -d --config=$conf
    # Run with system Thin
      RAILS_MASTER_KEY=$MASTER_KEY $DAEMON $1 -d --config=$conf
case "$1" in
  invoke start $2
  invoke stop $2
  invoke restart $2
  echo "Usage: $SCRIPT_NAME {start|stop|restart} [config_name]" >&2
  exit 3

And it still works after I’ve taken back ownership of master.key and set its permission to 0600. I’ve also confirmed that I can run bundle exec rails c as myself without any permission errors. I think that’s all I need to do?

This is not a real answer to your question, merely a suggestion:

In production, don’t store critical credentials in files on the server. Rather, put them in environment variables, then launch the services that need the credentials, then delete the values from the environment variables again (so they are not printed out, should the environment variables somehow be dumped one day, due to some potential unknown/unresolved bug somewhere).

Much safer.

1 Like

Thanks - that’s basically what I’m doing, though the “critical credentials” in this case is the decryption key for credentials.yml.enc. Perhaps I’m misunderstanding something, but I don’t see how it would be possible to put anything into an environment variable without that value being present in a file of some kind?

1 Like

In your config/database.yml, you would have something like:

  <<: *default
  database: yourapp_production
  username: <%= ENV["DATABASE_USR"] %>
  password: <%= ENV["DATABASE_PWD"] %>

And then, you’d start your app manually, by feeding the environment variables ENV["DATABASE_USR"] etc. to the process (and then also emptying these variables once everything’s running correctly).

So yes, it does require a manual intervention to start or restart your server, but security always has a “cost” (trade-off: security ↔ convenience).

Ah yes, that’s what I thought. I guess I could live with that, though I’d still want to use the credentials.yml.enc file and pass the key to that instead. My app uses other credentials besides the postgres login, and it would be tedious to have to supply them all manually. I assume the encryption method used is secure enough, though a quick search failed to identify the precise method used?

I assume Thin will do this automagically.

No need to. What I do is this: For security reasons, I have a “dedicated” lightweigh/minimal $500 notebook which I use exclusively to connect to my server. As soon as the server is up OK, I immediately take it offline and do not use if for anything else, which minimizes exposure to potential online attacks.

So you don’t have to type in all your credentials - you just save the launch command containing all your ENV variables/values in a text file, and then copy/paste it into your ssh shell, each time you need to launch your server. Since this machine is online only a couple of minutes and you use it only for this, you can consider this as-safe-as-it-gets.

(And I don’t think Thin would empty automatically all ENV variables you set, I assume you need to empty them manually. But you could also write a shell script that would contain your server launch command with all the ENV variables, and which would then also have the command to empty those ENV variables at the end - once it has checked that all required services are running OK.)

I was thinking specifically about RAILS_MASTER_KEY. At least I think Puma does this.

1 last thing to always remember: Whenever you use/set credentials in shell commands, always be 100% sure that no command history whatsoever is being saved in your shell (this normally requires configuration).

1 Like