-
-
Notifications
You must be signed in to change notification settings - Fork 15.3k
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
Conversation
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') |
There was a problem hiding this comment.
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 :)
There was a problem hiding this comment.
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}) |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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 :-)
There was a problem hiding this comment.
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 ;)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Hehe :-)
There was a problem hiding this comment.
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 |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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'" |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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'
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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'"
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
How about 4c9744b ?
There was a problem hiding this comment.
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; |
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.)
There was a problem hiding this comment.
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).
@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}") |
There was a problem hiding this comment.
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"
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
See https://stackoverflow.com/a/43850372/133235 as a reference
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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...
There was a problem hiding this comment.
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. :)
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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!
There was a problem hiding this comment.
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?
I have one other request for this patch (and thank you for the work!) -- can you add It's rare for a NixOS service to do this, but there is precedent: c.f.,
|
@dhess Like so? |
Perfect, thank you! |
I believe this looks good after I've fix the merge conflicts. @domenkozar Are you satisfied with the changes? |
a9f9e26
to
2c79f06
Compare
Bump? Would be nice to have this merged. |
Can we get this merged? :) |
@srhb @domenkozar What has to be done to get this (rebased) and merged? It looks pretty good. Is the lack of a |
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. |
2c79f06
to
f2fb706
Compare
@@ -368,7 +350,7 @@ in { | |||
}; | |||
|
|||
secrets.secret = mkOption { | |||
type = types.str; | |||
type = types.path; |
There was a problem hiding this comment.
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 eventypes.changed "<reason, and remedy>" types.str types.path
. I quite like this one.
I don't consider this a blocker for this PR though.
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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
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; |
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? |
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 |
This has been implemented |
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:
types.path
solely, which could conceivably fail.Things done
build-use-sandbox
innix.conf
on non-NixOS)nix-shell -p nox --run "nox-review wip"
./result/bin/
)