Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

nixos/gitlab: Store secrets in files rather than the store #31358

Closed
wants to merge 1 commit into from

Conversation

srhb
Copy link
Contributor

@srhb srhb commented Nov 7, 2017

Motivation for this change

The current gitlab module stores all secrets in the store, rendering them world readable. This PR converts databasePassword and secrets.* to types.path and ensures that preStart substitutes them into place. The NixOS test has also been altered to write dummy secrets to the store (but it still times out in qemu, which I've not been able to fix.)

There are some things I would like to improve:

  • Better error on the change. Right now, I'm relying on types.path solely, which could conceivably fail.
  • The database password is still emitted in the journal upon creation. This should be safe since normal users cannot read it, though
  • The substitution feels brittle. I am not a sed wizard.
Things done
  • Tested using sandboxing (nix.useSandbox on NixOS, or option build-use-sandbox in nix.conf on non-NixOS)
  • Built on platform(s)
    • NixOS
    • macOS
    • other Linux distributions
  • Tested via one or more NixOS test(s) if existing and applicable for the change (look inside nixos/tests)
  • Tested compilation of all pkgs that depend on this change using nix-shell -p nox --run "nox-review wip"
  • Tested execution of all binary files (usually in ./result/bin/)
  • Fits CONTRIBUTING.md.

@globin
Copy link
Member

globin commented Nov 7, 2017

This at least needs an entry in the release-notes, also cc @fpletz

SECRETKEY=$(cat ${cfg.secrets.secret})
OTPKEY=$(cat ${cfg.secrets.otp})
DBKEY=$(cat ${cfg.secrets.db})
JWSKEY=$(${pkgs.jq}/bin/jq -Rs . ${cfg.secrets.jws} | sed -e 's,\\n,\\\\n,g')
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What about other escape codes, should they exist in the string? I don't really know if they will, but it's really not obvious, at least to me. To be fair I'm not really sure what the JWS key is :)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The JWS key is actually the only one that's reasonably safe to assume doesn't contain a comma ( :( ), because it's base64 encoded...

ln -fs ${pkgs.writeText "unicorn.rb" unicornConfig} ${cfg.statePath}/config/unicorn.rb

chown -R ${cfg.user}:${cfg.group} ${cfg.statePath}/
chmod -R ug+rwX,o-rwx+X ${cfg.statePath}/

DBPASSWORD=$(cat ${cfg.databasePassword})
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should probably be quoted to prevent whitespace in the password from breaking.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actually never mind, this seems to be fine.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This quoting is needed for databasePassword to tolerate whitespace: DBPASSWORD=$(cat "${cfg.databasePassword}"). The "outer" quoting is unneeded, as mentioned, although it looks a bit scary :-)

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Although while putting spaces in passwords is a sensible thing to do IMHO, anyone who's putting a space in the path to their password should reevaluate their life choices. Again IMHO ;)

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hehe :-)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This at least can be trivially fixed, and I'll do so. Thanks!

sed -e "s,#secretkeyplaceholder#,$SECRETKEY,g" -i ${cfg.statePath}/config/secrets.yml
sed -e "s,#otpkeyplaceholder#,$OTPKEY,g" -i ${cfg.statePath}/config/secrets.yml
sed -e "s,#dbkeyplaceholder#,$DBKEY,g" -i ${cfg.statePath}/config/secrets.yml
sed -e "s,#jwskeyplaceholder#,$JWSKEY,g" -i ${cfg.statePath}/config/secrets.yml
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

All these substitutions will break if any of the keys/passwords contain a comma, right? Perhaps use ${VAR//,/\\,/} rather than $VAR in each case.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If this seems like an acceptable fix to the substitution issue, I'll do it. I feel like the entire sed approach is wrong, but I don't know of any other reasonable alternatives. :/

# Install the shell required to push repositories
ln -fs ${pkgs.writeText "config.yml" gitlabShellYml} "$GITLAB_SHELL_CONFIG_PATH"
ln -fs ${cfg.packages.gitlab-shell}/hooks "$GITLAB_SHELL_HOOKS_PATH"
${cfg.packages.gitlab-shell}/bin/install

if [ "${cfg.databaseHost}" = "127.0.0.1" ]; then
if ! test -e "${cfg.statePath}/db-created"; then
${pkgs.sudo}/bin/sudo -u ${pgSuperUser} psql postgres -c "CREATE ROLE ${cfg.databaseUsername} WITH LOGIN NOCREATEDB NOCREATEROLE ENCRYPTED PASSWORD '${cfg.databasePassword}'"
${pkgs.sudo}/bin/sudo -u ${pgSuperUser} psql postgres -c "CREATE ROLE ${cfg.databaseUsername} WITH LOGIN NOCREATEDB NOCREATEROLE ENCRYPTED PASSWORD '$DBPASSWORD'"
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This would break with a password containing a quote, right? Similarly to before, I'd suggest ${DBPASSWORD//'/\\'/}… Although I'm guessing that would still go wrong in other ways 😢 (I wouldn't be surprised if postgres interprets other escape sequences too.) Oh, the joys of substituting text into other languages.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Also, shouldn't bash refrain from variable substitutions in single quotes anyway?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Uh, yes. I must have been fooled by the peer authentication.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh, no, this is fine.

$ foo=test; echo "This is fine: '$foo'"
This is fine: 'test'

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hm, weird. Without the outer quotes:

foo=test; echo This is fine: '$foo'
This is fine: $foo

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah, I guess that has the same problem that @dhess pointed out.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, because it would appear in ps, just for echo rather than psql. You could also remove the -, since psql won't care about the leading whitespace AFAIK.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Another alternative is psql postgres <<< "CREATE ROLE ${cfg.databaseUsername} WITH LOGIN NOCREATEDB NOCREATEROLE ENCRYPTED PASSWORD '$DBPASSWORD'"

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

How about 4c9744b ?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM!

@@ -247,8 +247,7 @@ in {
};

databasePassword = mkOption {
type = types.str;
default = "";
type = types.path;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

aren't paths copied into the nix store as well? I stumbled upon this in #29298

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As far as I understand, neither the value of type str or path is copied directly to the store. The problem is that the secretsYml file was. Instead, that file (which is still copied to the store) is merely a template which we copy out substitute into at runtime from files whose path are specified here.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actually, I suppose the configuration is stored, but it doesn't matter, since the path, not the contents is what's made world-readable. At best you can now see where the secret is stored, but it should still be secure (if permissions are correct.)

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@srhb good work on making NixOS more secure! With regards to password options having type path see this comment by @domenkozar: #24288 (comment).

@srhb
Copy link
Contributor Author

srhb commented Nov 8, 2017

@lheckemann pointed out that bash is pretty good at substitutions, so let's try that instead. >_>

The only outstanding issue is really the createrole where single quotes will cause issues.

ln -fs ${pkgs.writeText "unicorn.rb" unicornConfig} ${cfg.statePath}/config/unicorn.rb

chown -R ${cfg.user}:${cfg.group} ${cfg.statePath}/
chmod -R ug+rwX,o-rwx+X ${cfg.statePath}/

cat > ${cfg.statePath}/config/secrets.yml << EOF
production:
secret_key_base: $(cat "${cfg.secrets.secret}")
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Does this really prevent store path being created? I think you should do:

nix-repl> "${toString ./timeout.txt}"
"/home/ielectric/timeout.txt"

instead of

nix-repl> "${./timeout.txt}"
"/nix/store/qvggx3asvcz26r9wpclpy67f0k5zryg6-timeout.txt"

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm sorry, I don't understand exactly where you think it's going wrong. Can you point me at the exact spot? Here's an example of the rendered preStart-script:

cat > /var/gitlab/state/config/secrets.yml << EOF
production:
  secret_key_base: $(cat "/run/keys/secret")
  otp_key_base: $(cat "/run/keys/otp")
  db_key_base: $(cat "/run/keys/db")
  jws_private_key: $(/nix/store/dc4f9mfgbwb0wygwk7qw5b0s0mv7kcml-jq-1.5/bin/jq -Rs . "/run/keys/jws")
EOF

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is with services.gitlab.secrets.db set to "/run/keys/db" and so on.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The problem is, someone can specify it as /run/keys/db instead of "/run/keys/db" and it would get copied to the store if you don't call toString first.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good point! That's actually a fair argument against using path at all...

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

0ba5f49 incorporates your suggestion, thank you. :)

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We could come up with a type like nonStorePath meaning it would validate the value is not in store but is a path.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think that would actually be pretty handy!

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@domenkozar Do you still want nonStorePath for this PR?

@dhess
Copy link
Contributor

dhess commented Dec 2, 2017

I have one other request for this patch (and thank you for the work!) -- can you add after and wants dependencies on "keys.target", so that those of us who use NixOps can safely deploy these secrets via deployment.keys?

It's rare for a NixOS service to do this, but there is precedent: c.f.,

after = [ "keys.target" "network.target" ];

@srhb
Copy link
Contributor Author

srhb commented Dec 5, 2017

@dhess Like so?

@dhess
Copy link
Contributor

dhess commented Dec 5, 2017

Perfect, thank you!

@srhb
Copy link
Contributor Author

srhb commented Jan 5, 2018

I believe this looks good after I've fix the merge conflicts.

@domenkozar Are you satisfied with the changes?

@srhb srhb force-pushed the gitlab-secure-wip branch 2 times, most recently from a9f9e26 to 2c79f06 Compare January 8, 2018 14:39
@lheckemann
Copy link
Member

Bump? Would be nice to have this merged.

@arianvp
Copy link
Member

arianvp commented Aug 6, 2018

Can we get this merged? :)

@matthewbauer matthewbauer modified the milestones: 18.03, 18.09 Oct 1, 2018
@andir
Copy link
Member

andir commented Oct 6, 2018

@srhb @domenkozar What has to be done to get this (rebased) and merged? It looks pretty good.

Is the lack of a nonStorePath a blocker?

@samueldr samueldr modified the milestones: 18.09, 19.03 Oct 6, 2018
@srhb
Copy link
Contributor Author

srhb commented Oct 6, 2018

I no longer use gitlab, so I'm not in a position to test, but I can do a blind rebase and someone else can take over from there.

@@ -368,7 +350,7 @@ in {
};

secrets.secret = mkOption {
type = types.str;
type = types.path;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

One thing I don't like is how this breaks everybody's config with a mediocre error message (type mismatch). Alternatives are:

  • Introduce {secret,db,...}File options for path based passwords. I don't like this because it introduces more options.
  • Introduce a type types.changed types.str types.path for such situations, maybe even types.changed "<reason, and remedy>" types.str types.path. I quite like this one.

I don't consider this a blocker for this PR though.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@infinisil, in nextcloud you have the adminPass and adminPassFile options next to each other. I think that's quite an ok solution.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As per infinisil's suggestion

Suggested change
type = types.path;
type = types.changed "The secret is now stored in a file, rather than plain text, to avoid it being world-visible." types.str types.path;

@flokli
Copy link
Contributor

flokli commented Nov 3, 2018

@globin @fpletz Could you take over?

@globin
Copy link
Member

globin commented Nov 3, 2018

Could you make this work with either a file or the previous in-store-secrets to not break gitlab for anyone currently using it?

@turion
Copy link
Contributor

turion commented Nov 11, 2018

Could you make this work with either a file or the previous in-store-secrets to not break gitlab for anyone currently using it?

What currently breaks?

@turion
Copy link
Contributor

turion commented Nov 11, 2018

I'd like to work on this as I want to host my own gitlab instance, but I'm not very good at nix still.

One other problem with having the secret in the config is that you can't upload your config anywhere without "cleaning" it first. It would be great if there was a mkSecretOption or so that would automatically correspond to a path with the correct permissions where the secret is generated and stored.

@FRidh FRidh modified the milestones: 19.03, 19.09 Mar 3, 2019
@globin
Copy link
Member

globin commented Oct 22, 2019

This has been implemented

@globin globin closed this Oct 22, 2019
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet