Merge pull request #66274 from talyz/gitlab

nixos/gitlab: Add support for secure secrets and more
This commit is contained in:
Florian Klink
2019-09-07 12:52:44 -07:00
committed by GitHub
4 changed files with 374 additions and 159 deletions
+240 -83
View File
@@ -1,4 +1,4 @@
{ config, lib, pkgs, ... }:
{ config, lib, pkgs, utils, ... }:
# TODO: support non-postgresql
@@ -12,14 +12,12 @@ let
gitlabSocket = "${cfg.statePath}/tmp/sockets/gitlab.socket";
gitalySocket = "${cfg.statePath}/tmp/sockets/gitaly.socket";
pathUrlQuote = url: replaceStrings ["/"] ["%2F"] url;
pgSuperUser = config.services.postgresql.superUser;
databaseConfig = {
production = {
adapter = "postgresql";
database = cfg.databaseName;
host = cfg.databaseHost;
password = cfg.databasePassword;
username = cfg.databaseUsername;
encoding = "utf8";
pool = cfg.databasePool;
@@ -66,13 +64,6 @@ let
redisConfig.production.url = "redis://localhost:6379/";
secretsConfig.production = {
secret_key_base = cfg.secrets.secret;
otp_key_base = cfg.secrets.otp;
db_key_base = cfg.secrets.db;
openid_connect_signing_key = cfg.secrets.jws;
};
gitlabConfig = {
# These are the default settings from config/gitlab.example.yml
production = flip recursiveUpdate cfg.extraConfig {
@@ -180,10 +171,11 @@ let
address: "${cfg.smtp.address}",
port: ${toString cfg.smtp.port},
${optionalString (cfg.smtp.username != null) ''user_name: "${cfg.smtp.username}",''}
${optionalString (cfg.smtp.password != null) ''password: "${cfg.smtp.password}",''}
${optionalString (cfg.smtp.passwordFile != null) ''password: "@smtpPassword@",''}
domain: "${cfg.smtp.domain}",
${optionalString (cfg.smtp.authentication != null) "authentication: :${cfg.smtp.authentication},"}
enable_starttls_auto: ${toString cfg.smtp.enableStartTLSAuto},
ca_file: "/etc/ssl/certs/ca-certificates.crt",
openssl_verify_mode: '${cfg.smtp.opensslVerifyMode}'
}
end
@@ -244,13 +236,33 @@ in {
databaseHost = mkOption {
type = types.str;
default = "127.0.0.1";
description = "Gitlab database hostname.";
default = "";
description = ''
Gitlab database hostname. An empty string means <quote>use
local unix socket connection</quote>.
'';
};
databasePassword = mkOption {
type = types.str;
description = "Gitlab database user password.";
databasePasswordFile = mkOption {
type = with types; nullOr path;
default = null;
description = ''
File containing the Gitlab database user password.
This should be a string, not a nix path, since nix paths are
copied into the world-readable nix store.
'';
};
databaseCreateLocally = mkOption {
type = types.bool;
default = true;
description = ''
Whether a database should be automatically created on the
local host. Set this to <literal>false</literal> if you plan
on provisioning a local database yourself or use an external
one.
'';
};
databaseName = mkOption {
@@ -338,10 +350,15 @@ in {
'';
};
initialRootPassword = mkOption {
type = types.str;
initialRootPasswordFile = mkOption {
type = with types; nullOr path;
default = null;
description = ''
Initial password of the root account if this is a new install.
File containing the initial password of the root account if
this is a new install.
This should be a string, not a nix path, since nix paths are
copied into the world-readable nix store.
'';
};
@@ -365,15 +382,20 @@ in {
};
username = mkOption {
type = types.nullOr types.str;
type = with types; nullOr str;
default = null;
description = "Username of the SMTP server for Gitlab.";
};
password = mkOption {
type = types.nullOr types.str;
passwordFile = mkOption {
type = types.nullOr types.path;
default = null;
description = "Password of the SMTP server for Gitlab.";
description = ''
File containing the password of the SMTP server for Gitlab.
This should be a string, not a nix path, since nix paths
are copied into the world-readable nix store.
'';
};
domain = mkOption {
@@ -383,7 +405,7 @@ in {
};
authentication = mkOption {
type = types.nullOr types.str;
type = with types; nullOr str;
default = null;
description = "Authentitcation type to use, see http://api.rubyonrails.org/classes/ActionMailer/Base.html";
};
@@ -401,68 +423,125 @@ in {
};
};
secrets.secret = mkOption {
type = types.str;
secrets.secretFile = mkOption {
type = with types; nullOr path;
default = null;
description = ''
The secret is used to encrypt variables in the DB. If
you change or lose this key you will be unable to access variables
stored in database.
A file containing the secret used to encrypt variables in
the DB. If you change or lose this key you will be unable to
access variables stored in database.
Make sure the secret is at least 30 characters and all random,
no regular words or you'll be exposed to dictionary attacks.
This should be a string, not a nix path, since nix paths are
copied into the world-readable nix store.
'';
};
secrets.db = mkOption {
type = types.str;
secrets.dbFile = mkOption {
type = with types; nullOr path;
default = null;
description = ''
The secret is used to encrypt variables in the DB. If
you change or lose this key you will be unable to access variables
stored in database.
A file containing the secret used to encrypt variables in
the DB. If you change or lose this key you will be unable to
access variables stored in database.
Make sure the secret is at least 30 characters and all random,
no regular words or you'll be exposed to dictionary attacks.
This should be a string, not a nix path, since nix paths are
copied into the world-readable nix store.
'';
};
secrets.otp = mkOption {
type = types.str;
secrets.otpFile = mkOption {
type = with types; nullOr path;
default = null;
description = ''
The secret is used to encrypt secrets for OTP tokens. If
you change or lose this key, users which have 2FA enabled for login
won't be able to login anymore.
A file containing the secret used to encrypt secrets for OTP
tokens. If you change or lose this key, users which have 2FA
enabled for login won't be able to login anymore.
Make sure the secret is at least 30 characters and all random,
no regular words or you'll be exposed to dictionary attacks.
This should be a string, not a nix path, since nix paths are
copied into the world-readable nix store.
'';
};
secrets.jws = mkOption {
type = types.str;
secrets.jwsFile = mkOption {
type = with types; nullOr path;
default = null;
description = ''
The secret is used to encrypt session keys. If you change or lose
this key, users will be disconnected.
A file containing the secret used to encrypt session
keys. If you change or lose this key, users will be
disconnected.
Make sure the secret is an RSA private key in PEM format. You can
generate one with
openssl genrsa 2048
This should be a string, not a nix path, since nix paths are
copied into the world-readable nix store.
'';
};
extraConfig = mkOption {
type = types.attrs;
default = {};
example = {
gitlab = {
default_projects_features = {
builds = false;
example = literalExample ''
{
gitlab = {
default_projects_features = {
builds = false;
};
};
omniauth = {
enabled = true;
auto_sign_in_with_provider = "openid_connect";
allow_single_sign_on = ["openid_connect"];
block_auto_created_users = false;
providers = [
{
name = "openid_connect";
label = "OpenID Connect";
args = {
name = "openid_connect";
scope = ["openid" "profile"];
response_type = "code";
issuer = "https://keycloak.example.com/auth/realms/My%20Realm";
discovery = true;
client_auth_method = "query";
uid_field = "preferred_username";
client_options = {
identifier = "gitlab";
secret = { _secret = "/var/keys/gitlab_oidc_secret"; };
redirect_uri = "https://git.example.com/users/auth/openid_connect/callback";
};
};
}
];
};
};
};
'';
description = ''
Extra options to be merged into config/gitlab.yml as nix
attribute set.
Extra options to be added under
<literal>production</literal> in
<filename>config/gitlab.yml</filename>, as a nix attribute
set.
Options containing secret data should be set to an attribute
set containing the attribute <literal>_secret</literal> - a
string pointing to a file containing the value the option
should be set to. See the example to get a better picture of
this: in the resulting
<filename>config/gitlab.yml</filename> file, the
<literal>production.omniauth.providers[0].args.client_options.secret</literal>
key will be set to the contents of the
<filename>/var/keys/gitlab_oidc_secret</filename> file.
'';
};
};
@@ -470,12 +549,66 @@ in {
config = mkIf cfg.enable {
assertions = [
{
assertion = cfg.databaseCreateLocally -> (cfg.user == cfg.databaseUsername);
message = "For local automatic database provisioning services.gitlab.user and services.gitlab.databaseUsername should be identical.";
}
{
assertion = (cfg.databaseHost != "") -> (cfg.databasePasswordFile != null);
message = "When services.gitlab.databaseHost is customized, services.gitlab.databasePasswordFile must be set!";
}
{
assertion = cfg.initialRootPasswordFile != null;
message = "services.gitlab.initialRootPasswordFile must be set!";
}
{
assertion = cfg.secrets.secretFile != null;
message = "services.gitlab.secrets.secretFile must be set!";
}
{
assertion = cfg.secrets.dbFile != null;
message = "services.gitlab.secrets.dbFile must be set!";
}
{
assertion = cfg.secrets.otpFile != null;
message = "services.gitlab.secrets.otpFile must be set!";
}
{
assertion = cfg.secrets.jwsFile != null;
message = "services.gitlab.secrets.jwsFile must be set!";
}
];
environment.systemPackages = [ pkgs.git gitlab-rake gitlab-rails cfg.packages.gitlab-shell ];
# Redis is required for the sidekiq queue runner.
services.redis.enable = mkDefault true;
# We use postgres as the main data store.
services.postgresql.enable = mkDefault true;
services.postgresql = optionalAttrs cfg.databaseCreateLocally {
enable = true;
ensureUsers = singleton { name = cfg.databaseUsername; };
};
# The postgresql module doesn't currently support concepts like
# objects owners and extensions; for now we tack on what's needed
# here.
systemd.services.postgresql.postStart = mkAfter (optionalString cfg.databaseCreateLocally ''
$PSQL -tAc "SELECT 1 FROM pg_database WHERE datname = '${cfg.databaseName}'" | grep -q 1 || $PSQL -tAc 'CREATE DATABASE "${cfg.databaseName}" OWNER "${cfg.databaseUsername}"'
current_owner=$($PSQL -tAc "SELECT pg_catalog.pg_get_userbyid(datdba) FROM pg_catalog.pg_database WHERE datname = '${cfg.databaseName}'")
if [[ "$current_owner" != "${cfg.databaseUsername}" ]]; then
$PSQL -tAc 'ALTER DATABASE "${cfg.databaseName}" OWNER TO "${cfg.databaseUsername}"'
if [[ -e "${config.services.postgresql.dataDir}/.reassigning_${cfg.databaseName}" ]]; then
echo "Reassigning ownership of database ${cfg.databaseName} to user ${cfg.databaseUsername} failed on last boot. Failing..."
exit 1
fi
touch "${config.services.postgresql.dataDir}/.reassigning_${cfg.databaseName}"
$PSQL "${cfg.databaseName}" -tAc "REASSIGN OWNED BY \"$current_owner\" TO \"${cfg.databaseUsername}\""
rm "${config.services.postgresql.dataDir}/.reassigning_${cfg.databaseName}"
fi
$PSQL '${cfg.databaseName}' -tAc "CREATE EXTENSION IF NOT EXISTS pg_trgm"
'');
# Use postfix to send out mails.
services.postfix.enable = mkDefault true;
@@ -527,14 +660,9 @@ in {
"L+ /run/gitlab/shell-config.yml - - - - ${pkgs.writeText "config.yml" (builtins.toJSON gitlabShellConfig)}"
"L+ ${cfg.statePath}/config/gitlab.yml - - - - ${pkgs.writeText "gitlab.yml" (builtins.toJSON gitlabConfig)}"
"L+ ${cfg.statePath}/config/database.yml - - - - ${pkgs.writeText "database.yml" (builtins.toJSON databaseConfig)}"
"L+ ${cfg.statePath}/config/secrets.yml - - - - ${pkgs.writeText "secrets.yml" (builtins.toJSON secretsConfig)}"
"L+ ${cfg.statePath}/config/unicorn.rb - - - - ${./defaultUnicornConfig.rb}"
"L+ ${cfg.statePath}/config/initializers/extra-gitlab.rb - - - - ${extraGitlabRb}"
] ++ optional cfg.smtp.enable
"L+ ${cfg.statePath}/config/initializers/smtp_settings.rb - - - - ${smtpSettings}" ;
];
systemd.services.gitlab-sidekiq = {
after = [ "network.target" "redis.service" "gitlab.service" ];
@@ -626,46 +754,75 @@ in {
gnupg
];
preStart = ''
${pkgs.sudo}/bin/sudo -u ${cfg.user} cp -f ${cfg.packages.gitlab}/share/gitlab/VERSION ${cfg.statePath}/VERSION
${pkgs.sudo}/bin/sudo -u ${cfg.user} rm -rf ${cfg.statePath}/db/*
${pkgs.sudo}/bin/sudo -u ${cfg.user} cp -rf --no-preserve=mode ${cfg.packages.gitlab}/share/gitlab/config.dist/* ${cfg.statePath}/config
${pkgs.sudo}/bin/sudo -u ${cfg.user} cp -rf --no-preserve=mode ${cfg.packages.gitlab}/share/gitlab/db/* ${cfg.statePath}/db
cp -f ${cfg.packages.gitlab}/share/gitlab/VERSION ${cfg.statePath}/VERSION
rm -rf ${cfg.statePath}/db/*
cp -rf --no-preserve=mode ${cfg.packages.gitlab}/share/gitlab/config.dist/* ${cfg.statePath}/config
cp -rf --no-preserve=mode ${cfg.packages.gitlab}/share/gitlab/db/* ${cfg.statePath}/db
${pkgs.openssl}/bin/openssl rand -hex 32 > ${cfg.statePath}/gitlab_shell_secret
${cfg.packages.gitlab-shell}/bin/install
${pkgs.sudo}/bin/sudo -u ${cfg.user} ${cfg.packages.gitlab-shell}/bin/install
${optionalString cfg.smtp.enable ''
install -m u=rw ${smtpSettings} ${cfg.statePath}/config/initializers/smtp_settings.rb
${optionalString (cfg.smtp.passwordFile != null) ''
smtp_password=$(<'${cfg.smtp.passwordFile}')
${pkgs.replace}/bin/replace-literal -e '@smtpPassword@' "$smtp_password" '${cfg.statePath}/config/initializers/smtp_settings.rb'
''}
''}
if ! test -e "${cfg.statePath}/db-created"; then
if [ "${cfg.databaseHost}" = "127.0.0.1" ]; 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} ${config.services.postgresql.package}/bin/createdb --owner ${cfg.databaseUsername} ${cfg.databaseName}
(
umask u=rwx,g=,o=
# enable required pg_trgm extension for gitlab
${pkgs.sudo}/bin/sudo -u ${pgSuperUser} psql ${cfg.databaseName} -c "CREATE EXTENSION IF NOT EXISTS pg_trgm"
${pkgs.openssl}/bin/openssl rand -hex 32 > ${cfg.statePath}/gitlab_shell_secret
${if cfg.databasePasswordFile != null then ''
export db_password="$(<'${cfg.databasePasswordFile}')"
if [[ -z "$db_password" ]]; then
>&2 echo "Database password was an empty string!"
exit 1
fi
${pkgs.jq}/bin/jq <${pkgs.writeText "database.yml" (builtins.toJSON databaseConfig)} \
'.production.password = $ENV.db_password' \
>'${cfg.statePath}/config/database.yml'
''
else ''
${pkgs.jq}/bin/jq <${pkgs.writeText "database.yml" (builtins.toJSON databaseConfig)} \
>'${cfg.statePath}/config/database.yml'
''
}
${utils.genJqSecretsReplacementSnippet
gitlabConfig
"${cfg.statePath}/config/gitlab.yml"
}
if [[ -h '${cfg.statePath}/config/secrets.yml' ]]; then
rm '${cfg.statePath}/config/secrets.yml'
fi
${pkgs.sudo}/bin/sudo -u ${cfg.user} -H ${gitlab-rake}/bin/gitlab-rake db:schema:load
export secret="$(<'${cfg.secrets.secretFile}')"
export db="$(<'${cfg.secrets.dbFile}')"
export otp="$(<'${cfg.secrets.otpFile}')"
export jws="$(<'${cfg.secrets.jwsFile}')"
${pkgs.jq}/bin/jq -n '{production: {secret_key_base: $ENV.secret,
otp_key_base: $ENV.db,
db_key_base: $ENV.otp,
openid_connect_signing_key: $ENV.jws}}' \
> '${cfg.statePath}/config/secrets.yml'
)
${pkgs.sudo}/bin/sudo -u ${cfg.user} touch "${cfg.statePath}/db-created"
fi
# Always do the db migrations just to be sure the database is up-to-date
${pkgs.sudo}/bin/sudo -u ${cfg.user} -H ${gitlab-rake}/bin/gitlab-rake db:migrate
if ! test -e "${cfg.statePath}/db-seeded"; then
${pkgs.sudo}/bin/sudo -u ${cfg.user} ${gitlab-rake}/bin/gitlab-rake db:seed_fu \
GITLAB_ROOT_PASSWORD='${cfg.initialRootPassword}' GITLAB_ROOT_EMAIL='${cfg.initialRootEmail}'
${pkgs.sudo}/bin/sudo -u ${cfg.user} touch "${cfg.statePath}/db-seeded"
fi
initial_root_password="$(<'${cfg.initialRootPasswordFile}')"
${gitlab-rake}/bin/gitlab-rake gitlab:db:configure GITLAB_ROOT_PASSWORD="$initial_root_password" \
GITLAB_ROOT_EMAIL='${cfg.initialRootEmail}'
# We remove potentially broken links to old gitlab-shell versions
rm -Rf ${cfg.statePath}/repositories/**/*.git/hooks
${pkgs.sudo}/bin/sudo -u ${cfg.user} -H ${pkgs.git}/bin/git config --global core.autocrlf "input"
${pkgs.git}/bin/git config --global core.autocrlf "input"
'';
serviceConfig = {
PermissionsStartOnly = true; # preStart must be run as root
Type = "simple";
User = cfg.user;
Group = cfg.group;