Merge pull request #66274 from talyz/gitlab
nixos/gitlab: Add support for secure secrets and more
This commit is contained in:
commit
2f3b9cd52c
|
@ -24,4 +24,116 @@ pkgs: with pkgs.lib;
|
||||||
throw "${shell} is not a shell package"
|
throw "${shell} is not a shell package"
|
||||||
else
|
else
|
||||||
shell;
|
shell;
|
||||||
|
|
||||||
|
/* Recurse into a list or an attrset, searching for attrs named like
|
||||||
|
the value of the "attr" parameter, and return an attrset where the
|
||||||
|
names are the corresponding jq path where the attrs were found and
|
||||||
|
the values are the values of the attrs.
|
||||||
|
|
||||||
|
Example:
|
||||||
|
recursiveGetAttrWithJqPrefix {
|
||||||
|
example = [
|
||||||
|
{
|
||||||
|
irrelevant = "not interesting";
|
||||||
|
}
|
||||||
|
{
|
||||||
|
ignored = "ignored attr";
|
||||||
|
relevant = {
|
||||||
|
secret = {
|
||||||
|
_secret = "/path/to/secret";
|
||||||
|
};
|
||||||
|
};
|
||||||
|
}
|
||||||
|
];
|
||||||
|
} "_secret" -> { ".example[1].relevant.secret" = "/path/to/secret"; }
|
||||||
|
*/
|
||||||
|
recursiveGetAttrWithJqPrefix = item: attr:
|
||||||
|
let
|
||||||
|
recurse = prefix: item:
|
||||||
|
if item ? ${attr} then
|
||||||
|
nameValuePair prefix item.${attr}
|
||||||
|
else if isAttrs item then
|
||||||
|
map (name: recurse (prefix + "." + name) item.${name}) (attrNames item)
|
||||||
|
else if isList item then
|
||||||
|
imap0 (index: item: recurse (prefix + "[${toString index}]") item) item
|
||||||
|
else
|
||||||
|
[];
|
||||||
|
in listToAttrs (flatten (recurse "" item));
|
||||||
|
|
||||||
|
/* Takes an attrset and a file path and generates a bash snippet that
|
||||||
|
outputs a JSON file at the file path with all instances of
|
||||||
|
|
||||||
|
{ _secret = "/path/to/secret" }
|
||||||
|
|
||||||
|
in the attrset replaced with the contents of the file
|
||||||
|
"/path/to/secret" in the output JSON.
|
||||||
|
|
||||||
|
When a configuration option accepts an attrset that is finally
|
||||||
|
converted to JSON, this makes it possible to let the user define
|
||||||
|
arbitrary secret values.
|
||||||
|
|
||||||
|
Example:
|
||||||
|
If the file "/path/to/secret" contains the string
|
||||||
|
"topsecretpassword1234",
|
||||||
|
|
||||||
|
genJqSecretsReplacementSnippet {
|
||||||
|
example = [
|
||||||
|
{
|
||||||
|
irrelevant = "not interesting";
|
||||||
|
}
|
||||||
|
{
|
||||||
|
ignored = "ignored attr";
|
||||||
|
relevant = {
|
||||||
|
secret = {
|
||||||
|
_secret = "/path/to/secret";
|
||||||
|
};
|
||||||
|
};
|
||||||
|
}
|
||||||
|
];
|
||||||
|
} "/path/to/output.json"
|
||||||
|
|
||||||
|
would generate a snippet that, when run, outputs the following
|
||||||
|
JSON file at "/path/to/output.json":
|
||||||
|
|
||||||
|
{
|
||||||
|
"example": [
|
||||||
|
{
|
||||||
|
"irrelevant": "not interesting"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ignored": "ignored attr",
|
||||||
|
"relevant": {
|
||||||
|
"secret": "topsecretpassword1234"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
*/
|
||||||
|
genJqSecretsReplacementSnippet = genJqSecretsReplacementSnippet' "_secret";
|
||||||
|
|
||||||
|
# Like genJqSecretsReplacementSnippet, but allows the name of the
|
||||||
|
# attr which identifies the secret to be changed.
|
||||||
|
genJqSecretsReplacementSnippet' = attr: set: output:
|
||||||
|
let
|
||||||
|
secrets = recursiveGetAttrWithJqPrefix set attr;
|
||||||
|
in ''
|
||||||
|
if [[ -h '${output}' ]]; then
|
||||||
|
rm '${output}'
|
||||||
|
fi
|
||||||
|
''
|
||||||
|
+ concatStringsSep
|
||||||
|
"\n"
|
||||||
|
(imap1 (index: name: "export secret${toString index}=$(<'${secrets.${name}}')")
|
||||||
|
(attrNames secrets))
|
||||||
|
+ "\n"
|
||||||
|
+ "${pkgs.jq}/bin/jq >'${output}' '"
|
||||||
|
+ concatStringsSep
|
||||||
|
" | "
|
||||||
|
(imap1 (index: name: ''${name} = $ENV.secret${toString index}'')
|
||||||
|
(attrNames secrets))
|
||||||
|
+ ''
|
||||||
|
' <<'EOF'
|
||||||
|
${builtins.toJSON set}
|
||||||
|
EOF
|
||||||
|
'';
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
{ config, lib, pkgs, ... }:
|
{ config, lib, pkgs, utils, ... }:
|
||||||
|
|
||||||
# TODO: support non-postgresql
|
# TODO: support non-postgresql
|
||||||
|
|
||||||
|
@ -12,14 +12,12 @@ let
|
||||||
gitlabSocket = "${cfg.statePath}/tmp/sockets/gitlab.socket";
|
gitlabSocket = "${cfg.statePath}/tmp/sockets/gitlab.socket";
|
||||||
gitalySocket = "${cfg.statePath}/tmp/sockets/gitaly.socket";
|
gitalySocket = "${cfg.statePath}/tmp/sockets/gitaly.socket";
|
||||||
pathUrlQuote = url: replaceStrings ["/"] ["%2F"] url;
|
pathUrlQuote = url: replaceStrings ["/"] ["%2F"] url;
|
||||||
pgSuperUser = config.services.postgresql.superUser;
|
|
||||||
|
|
||||||
databaseConfig = {
|
databaseConfig = {
|
||||||
production = {
|
production = {
|
||||||
adapter = "postgresql";
|
adapter = "postgresql";
|
||||||
database = cfg.databaseName;
|
database = cfg.databaseName;
|
||||||
host = cfg.databaseHost;
|
host = cfg.databaseHost;
|
||||||
password = cfg.databasePassword;
|
|
||||||
username = cfg.databaseUsername;
|
username = cfg.databaseUsername;
|
||||||
encoding = "utf8";
|
encoding = "utf8";
|
||||||
pool = cfg.databasePool;
|
pool = cfg.databasePool;
|
||||||
|
@ -66,13 +64,6 @@ let
|
||||||
|
|
||||||
redisConfig.production.url = "redis://localhost:6379/";
|
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 = {
|
gitlabConfig = {
|
||||||
# These are the default settings from config/gitlab.example.yml
|
# These are the default settings from config/gitlab.example.yml
|
||||||
production = flip recursiveUpdate cfg.extraConfig {
|
production = flip recursiveUpdate cfg.extraConfig {
|
||||||
|
@ -180,10 +171,11 @@ let
|
||||||
address: "${cfg.smtp.address}",
|
address: "${cfg.smtp.address}",
|
||||||
port: ${toString cfg.smtp.port},
|
port: ${toString cfg.smtp.port},
|
||||||
${optionalString (cfg.smtp.username != null) ''user_name: "${cfg.smtp.username}",''}
|
${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}",
|
domain: "${cfg.smtp.domain}",
|
||||||
${optionalString (cfg.smtp.authentication != null) "authentication: :${cfg.smtp.authentication},"}
|
${optionalString (cfg.smtp.authentication != null) "authentication: :${cfg.smtp.authentication},"}
|
||||||
enable_starttls_auto: ${toString cfg.smtp.enableStartTLSAuto},
|
enable_starttls_auto: ${toString cfg.smtp.enableStartTLSAuto},
|
||||||
|
ca_file: "/etc/ssl/certs/ca-certificates.crt",
|
||||||
openssl_verify_mode: '${cfg.smtp.opensslVerifyMode}'
|
openssl_verify_mode: '${cfg.smtp.opensslVerifyMode}'
|
||||||
}
|
}
|
||||||
end
|
end
|
||||||
|
@ -244,13 +236,33 @@ in {
|
||||||
|
|
||||||
databaseHost = mkOption {
|
databaseHost = mkOption {
|
||||||
type = types.str;
|
type = types.str;
|
||||||
default = "127.0.0.1";
|
default = "";
|
||||||
description = "Gitlab database hostname.";
|
description = ''
|
||||||
|
Gitlab database hostname. An empty string means <quote>use
|
||||||
|
local unix socket connection</quote>.
|
||||||
|
'';
|
||||||
};
|
};
|
||||||
|
|
||||||
databasePassword = mkOption {
|
databasePasswordFile = mkOption {
|
||||||
type = types.str;
|
type = with types; nullOr path;
|
||||||
description = "Gitlab database user password.";
|
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 {
|
databaseName = mkOption {
|
||||||
|
@ -338,10 +350,15 @@ in {
|
||||||
'';
|
'';
|
||||||
};
|
};
|
||||||
|
|
||||||
initialRootPassword = mkOption {
|
initialRootPasswordFile = mkOption {
|
||||||
type = types.str;
|
type = with types; nullOr path;
|
||||||
|
default = null;
|
||||||
description = ''
|
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 {
|
username = mkOption {
|
||||||
type = types.nullOr types.str;
|
type = with types; nullOr str;
|
||||||
default = null;
|
default = null;
|
||||||
description = "Username of the SMTP server for Gitlab.";
|
description = "Username of the SMTP server for Gitlab.";
|
||||||
};
|
};
|
||||||
|
|
||||||
password = mkOption {
|
passwordFile = mkOption {
|
||||||
type = types.nullOr types.str;
|
type = types.nullOr types.path;
|
||||||
default = null;
|
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 {
|
domain = mkOption {
|
||||||
|
@ -383,7 +405,7 @@ in {
|
||||||
};
|
};
|
||||||
|
|
||||||
authentication = mkOption {
|
authentication = mkOption {
|
||||||
type = types.nullOr types.str;
|
type = with types; nullOr str;
|
||||||
default = null;
|
default = null;
|
||||||
description = "Authentitcation type to use, see http://api.rubyonrails.org/classes/ActionMailer/Base.html";
|
description = "Authentitcation type to use, see http://api.rubyonrails.org/classes/ActionMailer/Base.html";
|
||||||
};
|
};
|
||||||
|
@ -401,68 +423,125 @@ in {
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
secrets.secret = mkOption {
|
secrets.secretFile = mkOption {
|
||||||
type = types.str;
|
type = with types; nullOr path;
|
||||||
|
default = null;
|
||||||
description = ''
|
description = ''
|
||||||
The secret is used to encrypt variables in the DB. If
|
A file containing the secret used to encrypt variables in
|
||||||
you change or lose this key you will be unable to access variables
|
the DB. If you change or lose this key you will be unable to
|
||||||
stored in database.
|
access variables stored in database.
|
||||||
|
|
||||||
Make sure the secret is at least 30 characters and all random,
|
Make sure the secret is at least 30 characters and all random,
|
||||||
no regular words or you'll be exposed to dictionary attacks.
|
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 {
|
secrets.dbFile = mkOption {
|
||||||
type = types.str;
|
type = with types; nullOr path;
|
||||||
|
default = null;
|
||||||
description = ''
|
description = ''
|
||||||
The secret is used to encrypt variables in the DB. If
|
A file containing the secret used to encrypt variables in
|
||||||
you change or lose this key you will be unable to access variables
|
the DB. If you change or lose this key you will be unable to
|
||||||
stored in database.
|
access variables stored in database.
|
||||||
|
|
||||||
Make sure the secret is at least 30 characters and all random,
|
Make sure the secret is at least 30 characters and all random,
|
||||||
no regular words or you'll be exposed to dictionary attacks.
|
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 {
|
secrets.otpFile = mkOption {
|
||||||
type = types.str;
|
type = with types; nullOr path;
|
||||||
|
default = null;
|
||||||
description = ''
|
description = ''
|
||||||
The secret is used to encrypt secrets for OTP tokens. If
|
A file containing the secret used to encrypt secrets for OTP
|
||||||
you change or lose this key, users which have 2FA enabled for login
|
tokens. If you change or lose this key, users which have 2FA
|
||||||
won't be able to login anymore.
|
enabled for login won't be able to login anymore.
|
||||||
|
|
||||||
Make sure the secret is at least 30 characters and all random,
|
Make sure the secret is at least 30 characters and all random,
|
||||||
no regular words or you'll be exposed to dictionary attacks.
|
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 {
|
secrets.jwsFile = mkOption {
|
||||||
type = types.str;
|
type = with types; nullOr path;
|
||||||
|
default = null;
|
||||||
description = ''
|
description = ''
|
||||||
The secret is used to encrypt session keys. If you change or lose
|
A file containing the secret used to encrypt session
|
||||||
this key, users will be disconnected.
|
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
|
Make sure the secret is an RSA private key in PEM format. You can
|
||||||
generate one with
|
generate one with
|
||||||
|
|
||||||
openssl genrsa 2048
|
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 {
|
extraConfig = mkOption {
|
||||||
type = types.attrs;
|
type = types.attrs;
|
||||||
default = {};
|
default = {};
|
||||||
example = {
|
example = literalExample ''
|
||||||
gitlab = {
|
{
|
||||||
default_projects_features = {
|
gitlab = {
|
||||||
builds = false;
|
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 = ''
|
description = ''
|
||||||
Extra options to be merged into config/gitlab.yml as nix
|
Extra options to be added under
|
||||||
attribute set.
|
<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 {
|
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 ];
|
environment.systemPackages = [ pkgs.git gitlab-rake gitlab-rails cfg.packages.gitlab-shell ];
|
||||||
|
|
||||||
# Redis is required for the sidekiq queue runner.
|
# Redis is required for the sidekiq queue runner.
|
||||||
services.redis.enable = mkDefault true;
|
services.redis.enable = mkDefault true;
|
||||||
|
|
||||||
# We use postgres as the main data store.
|
# 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.
|
# Use postfix to send out mails.
|
||||||
services.postfix.enable = mkDefault true;
|
services.postfix.enable = mkDefault true;
|
||||||
|
|
||||||
|
@ -527,14 +660,9 @@ in {
|
||||||
|
|
||||||
"L+ /run/gitlab/shell-config.yml - - - - ${pkgs.writeText "config.yml" (builtins.toJSON gitlabShellConfig)}"
|
"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/unicorn.rb - - - - ${./defaultUnicornConfig.rb}"
|
||||||
|
|
||||||
"L+ ${cfg.statePath}/config/initializers/extra-gitlab.rb - - - - ${extraGitlabRb}"
|
"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 = {
|
systemd.services.gitlab-sidekiq = {
|
||||||
after = [ "network.target" "redis.service" "gitlab.service" ];
|
after = [ "network.target" "redis.service" "gitlab.service" ];
|
||||||
|
@ -626,46 +754,75 @@ in {
|
||||||
gnupg
|
gnupg
|
||||||
];
|
];
|
||||||
preStart = ''
|
preStart = ''
|
||||||
${pkgs.sudo}/bin/sudo -u ${cfg.user} cp -f ${cfg.packages.gitlab}/share/gitlab/VERSION ${cfg.statePath}/VERSION
|
cp -f ${cfg.packages.gitlab}/share/gitlab/VERSION ${cfg.statePath}/VERSION
|
||||||
${pkgs.sudo}/bin/sudo -u ${cfg.user} rm -rf ${cfg.statePath}/db/*
|
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
|
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 -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
|
umask u=rwx,g=,o=
|
||||||
${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}
|
|
||||||
|
|
||||||
# enable required pg_trgm extension for gitlab
|
${pkgs.openssl}/bin/openssl rand -hex 32 > ${cfg.statePath}/gitlab_shell_secret
|
||||||
${pkgs.sudo}/bin/sudo -u ${pgSuperUser} psql ${cfg.databaseName} -c "CREATE EXTENSION IF NOT EXISTS pg_trgm"
|
|
||||||
|
${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
|
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"
|
initial_root_password="$(<'${cfg.initialRootPasswordFile}')"
|
||||||
fi
|
${gitlab-rake}/bin/gitlab-rake gitlab:db:configure GITLAB_ROOT_PASSWORD="$initial_root_password" \
|
||||||
|
GITLAB_ROOT_EMAIL='${cfg.initialRootEmail}'
|
||||||
# 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
|
|
||||||
|
|
||||||
# We remove potentially broken links to old gitlab-shell versions
|
# We remove potentially broken links to old gitlab-shell versions
|
||||||
rm -Rf ${cfg.statePath}/repositories/**/*.git/hooks
|
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 = {
|
serviceConfig = {
|
||||||
PermissionsStartOnly = true; # preStart must be run as root
|
|
||||||
Type = "simple";
|
Type = "simple";
|
||||||
User = cfg.user;
|
User = cfg.user;
|
||||||
Group = cfg.group;
|
Group = cfg.group;
|
||||||
|
|
|
@ -54,8 +54,8 @@
|
||||||
<programlisting>
|
<programlisting>
|
||||||
services.gitlab = {
|
services.gitlab = {
|
||||||
<link linkend="opt-services.gitlab.enable">enable</link> = true;
|
<link linkend="opt-services.gitlab.enable">enable</link> = true;
|
||||||
<link linkend="opt-services.gitlab.databasePassword">databasePassword</link> = "eXaMpl3";
|
<link linkend="opt-services.gitlab.databasePasswordFile">databasePasswordFile</link> = "/var/keys/gitlab/db_password";
|
||||||
<link linkend="opt-services.gitlab.initialRootPassword">initialRootPassword</link> = "UseNixOS!";
|
<link linkend="opt-services.gitlab.initialRootPasswordFile">initialRootPasswordFile</link> = "/var/keys/gitlab/root_password";
|
||||||
<link linkend="opt-services.gitlab.https">https</link> = true;
|
<link linkend="opt-services.gitlab.https">https</link> = true;
|
||||||
<link linkend="opt-services.gitlab.host">host</link> = "git.example.com";
|
<link linkend="opt-services.gitlab.host">host</link> = "git.example.com";
|
||||||
<link linkend="opt-services.gitlab.port">port</link> = 443;
|
<link linkend="opt-services.gitlab.port">port</link> = 443;
|
||||||
|
@ -67,38 +67,10 @@ services.gitlab = {
|
||||||
<link linkend="opt-services.gitlab.smtp.port">port</link> = 25;
|
<link linkend="opt-services.gitlab.smtp.port">port</link> = 25;
|
||||||
};
|
};
|
||||||
secrets = {
|
secrets = {
|
||||||
<link linkend="opt-services.gitlab.secrets.db">db</link> = "uPgq1gtwwHiatiuE0YHqbGa5lEIXH7fMsvuTNgdzJi8P0Dg12gibTzBQbq5LT7PNzcc3BP9P1snHVnduqtGF43PgrQtU7XL93ts6gqe9CBNhjtaqUwutQUDkygP5NrV6";
|
<link linkend="opt-services.gitlab.secrets.dbFile">dbFile</link> = "/var/keys/gitlab/db";
|
||||||
<link linkend="opt-services.gitlab.secrets.secret">secret</link> = "devzJ0Tz0POiDBlrpWmcsjjrLaltyiAdS8TtgT9YNBOoUcDsfppiY3IXZjMVtKgXrFImIennFGOpPN8IkP8ATXpRgDD5rxVnKuTTwYQaci2NtaV1XxOQGjdIE50VGsR3";
|
<link linkend="opt-services.gitlab.secrets.secretFile">secretFile</link> = "/var/keys/gitlab/secret";
|
||||||
<link linkend="opt-services.gitlab.secrets.otp">otp</link> = "e1GATJVuS2sUh7jxiPzZPre4qtzGGaS22FR50Xs1TerRVdgI3CBVUi5XYtQ38W4xFeS4mDqi5cQjExE838iViSzCdcG19XSL6qNsfokQP9JugwiftmhmCadtsnHErBMI";
|
<link linkend="opt-services.gitlab.secrets.otpFile">otpFile</link> = "/var/keys/gitlab/otp";
|
||||||
<link linkend="opt-services.gitlab.secrets.jws">jws</link> = ''
|
<link linkend="opt-services.gitlab.secrets.jwsFile">jwsFile</link> = "/var/keys/gitlab/jws";
|
||||||
-----BEGIN RSA PRIVATE KEY-----
|
|
||||||
MIIEpAIBAAKCAQEArrtx4oHKwXoqUbMNqnHgAklnnuDon3XG5LJB35yPsXKv/8GK
|
|
||||||
ke92wkI+s1Xkvsp8tg9BIY/7c6YK4SR07EWL+dB5qwctsWR2Q8z+/BKmTx9D99pm
|
|
||||||
hnsjuNIXTF7BXrx3RX6BxZpH5Vzzh9nCwWKT/JCFqtwH7afNGGL7aMf+hdaiUg/Q
|
|
||||||
SD05yRObioiO4iXDolsJOhrnbZvlzVHl1ZYxFJv0H6/Snc0BBA9Fl/3uj6ANpbjP
|
|
||||||
eXF1SnJCqT87bj46r5NdVauzaRxAsIfqHroHK4UZ98X5LjGQFGvSqTvyjPBS4I1i
|
|
||||||
s7VJU28ObuutHxIxSlH0ibn4HZqWmKWlTS652wIDAQABAoIBAGtPcUTTw2sJlR3x
|
|
||||||
4k2wfAvLexkHNbZhBdKEa5JiO5mWPuLKwUiZEY2CU7Gd6csG3oqNWcm7/IjtC7dz
|
|
||||||
xV8p4yp8T4yq7vQIJ93B80NqTLtBD2QTvG2RCMJEPMzJUObWxkVmyVpLQyZo7KOd
|
|
||||||
KE/OM+aj94OUeEYLjRkSCScz1Gvq/qFG/nAy7KPCmN9JDHuhX26WHo2Rr1OnPNT/
|
|
||||||
7diph0bB9F3b8gjjNTqXDrpdAqVOgR/PsjEBz6DMY+bdyMIn87q2yfmMexxRofN6
|
|
||||||
LulpzSaa6Yup8N8H6PzVO6KAkQuf1aQRj0sMwGk1IZEnj6I0KbuHIZkw21Nc6sf2
|
|
||||||
ESFySDECgYEA1PnCNn5tmLnwe62Ttmrzl20zIS3Me1gUVJ1NTfr6+ai0I9iMYU21
|
|
||||||
5czuAjJPm9JKQF2vY8UAaCj2ZoObtHa/anb3xsCd8NXoM3iJq5JDoXI1ldz3Y+ad
|
|
||||||
U/bZUg1DLRvAniTuXmw9iOTwTwPxlDIGq5k+wG2Xmi1lk7zH8ezr9BMCgYEA0gfk
|
|
||||||
EhgcmPH8Z5cU3YYwOdt6HSJOM0OyN4k/5gnkv+HYVoJTj02gkrJmLr+mi1ugKj46
|
|
||||||
7huYO9TVnrKP21tmbaSv1dp5hS3letVRIxSloEtVGXmmdvJvBRzDWos+G+KcvADi
|
|
||||||
fFCz6w8v9NmO40CB7y/3SxTmSiSxDQeoi9LhDBkCgYEAsPgMWm25sfOnkY2NNUIv
|
|
||||||
wT8bAlHlHQT2d8zx5H9NttBpR3P0ShJhuF8N0sNthSQ7ULrIN5YGHYcUH+DyLAWU
|
|
||||||
TuomP3/kfa+xL7vUYb269tdJEYs4AkoppxBySoz8qenqpz422D0G8M6TpIS5Y5Qi
|
|
||||||
GMrQ6uLl21YnlpiCaFOfSQMCgYEAmZxj1kgEQmhZrnn1LL/D7czz1vMMNrpAUhXz
|
|
||||||
wg9iWmSXkU3oR1sDIceQrIhHCo2M6thwyU0tXjUft93pEQocM/zLDaGoVxtmRxxV
|
|
||||||
J08mg8IVD3jFoyFUyWxsBIDqgAKRl38eJsXvkO+ep3mm49Z+Ma3nM+apN3j2dQ0w
|
|
||||||
3HLzXaECgYBFLMEAboVFwi5+MZjGvqtpg2PVTisfuJy2eYnPwHs+AXUgi/xRNFjI
|
|
||||||
YHEa7UBPb5TEPSzWImQpETi2P5ywcUYL1EbN/nqPWmjFnat8wVmJtV4sUpJhubF4
|
|
||||||
Vqm9LxIWc1uQ1q1HDCejRIxIN3aSH+wgRS3Kcj8kCTIoXd1aERb04g==
|
|
||||||
-----END RSA PRIVATE KEY-----
|
|
||||||
'';
|
|
||||||
};
|
};
|
||||||
<link linkend="opt-services.gitlab.extraConfig">extraConfig</link> = {
|
<link linkend="opt-services.gitlab.extraConfig">extraConfig</link> = {
|
||||||
gitlab = {
|
gitlab = {
|
||||||
|
@ -113,12 +85,16 @@ services.gitlab = {
|
||||||
</para>
|
</para>
|
||||||
|
|
||||||
<para>
|
<para>
|
||||||
If you're setting up a new Gitlab instance, generate new secrets. You for
|
If you're setting up a new Gitlab instance, generate new
|
||||||
instance use <literal>tr -dc A-Za-z0-9 < /dev/urandom | head -c
|
secrets. You for instance use <literal>tr -dc A-Za-z0-9 <
|
||||||
128</literal> to generate a new secret. Gitlab encrypts sensitive data
|
/dev/urandom | head -c 128 > /var/keys/gitlab/db</literal> to
|
||||||
stored in the database. If you're restoring an existing Gitlab instance, you
|
generate a new db secret. Make sure the files can be read by, and
|
||||||
must specify the secrets secret from <literal>config/secrets.yml</literal>
|
only by, the user specified by <link
|
||||||
located in your Gitlab state folder.
|
linkend="opt-services.gitlab.user">services.gitlab.user</link>. Gitlab
|
||||||
|
encrypts sensitive data stored in the database. If you're restoring
|
||||||
|
an existing Gitlab instance, you must specify the secrets secret
|
||||||
|
from <literal>config/secrets.yml</literal> located in your Gitlab
|
||||||
|
state folder.
|
||||||
</para>
|
</para>
|
||||||
|
|
||||||
<para>
|
<para>
|
||||||
|
|
|
@ -29,44 +29,14 @@ import ./make-test.nix ({ pkgs, lib, ...} : with lib; {
|
||||||
|
|
||||||
services.gitlab = {
|
services.gitlab = {
|
||||||
enable = true;
|
enable = true;
|
||||||
databasePassword = "dbPassword";
|
databasePasswordFile = pkgs.writeText "dbPassword" "xo0daiF4";
|
||||||
inherit initialRootPassword;
|
initialRootPasswordFile = pkgs.writeText "rootPassword" initialRootPassword;
|
||||||
smtp.enable = true;
|
smtp.enable = true;
|
||||||
secrets = {
|
secrets = {
|
||||||
secret = "secret";
|
secretFile = pkgs.writeText "secret" "Aig5zaic";
|
||||||
otp = "otpsecret";
|
otpFile = pkgs.writeText "otpsecret" "Riew9mue";
|
||||||
db = "dbsecret";
|
dbFile = pkgs.writeText "dbsecret" "we2quaeZ";
|
||||||
|
jwsFile = pkgs.runCommand "oidcKeyBase" {} "${pkgs.openssl}/bin/openssl genrsa 2048 > $out";
|
||||||
# nix-shell -p openssl --run "openssl genrsa 2048"
|
|
||||||
jws = ''
|
|
||||||
-----BEGIN RSA PRIVATE KEY-----
|
|
||||||
MIIEpAIBAAKCAQEA13/qEio76OWUtWO0WIz9lWnsTWOU8Esv4sQHDq9PCEFsLt21
|
|
||||||
PAXrlWhLjjWcxGfsrDwnh7YErGHYL62BMSxMdFJolaknlQK/O/V8UETDe45VoHM+
|
|
||||||
Znk270RfUcfYFgiihnXUZXVmL0om9TsQSk646wCcjCY9LxtxUyKNhvT7KjgYw2aX
|
|
||||||
z34aw7M+Js3T2p1TjZPSC82GtmtKkJEKFMi5EjprLTDE7EdcUzr9Xuw+kQ+gRm9k
|
|
||||||
7FE+JQqSoprwE3Q0v2OAn3UhLMgg0gNFRnsc5l6IAshDzV+H22RPqKKlJjVjjfPY
|
|
||||||
0TQSvYLVApigHbDPH0BoCXfjFfQazbbP3OUHrwIDAQABAoIBAQCMU+tkcMQaYIV5
|
|
||||||
qLdjgkwO467QpivyXcOM8wF1eosIYTHFQvIlZ+WEoSmyLQ8shlADyBgls01Pw1c3
|
|
||||||
lNAv6RzQEmmwKzpvOh61OKH+0whIiOMRXHoh2IUBQZCgfHYlwvGyhUAN4WjtGmhM
|
|
||||||
AG4XNTQNM5S9Xpkw97nP3Qwz+YskbbkrfqtCEVy9ro+4nhbjqPsuO3adbnkva4zR
|
|
||||||
cyurRhrHgHU6LPjn5NHnHH4qw2faY2oAsL8pmpkTbO5IqWDvOcbjNfjVPgVoq26O
|
|
||||||
bbaa1qs4nmc80qQgMjRPJef535xyf3eLsSlDvpf6O8sPrJzVR1zaqEqixpQCZDac
|
|
||||||
+kRiSBrhAoGBAOwHiq0PuyJh6VzBu7ybqX6+gF/wA4Jkwzx6mbfaBgurvU1aospp
|
|
||||||
kisIonAkxSbxllZMnjbkShZEdATYKeT9o5NEhnU4YnHfc5bJZbiWOZAzYGLcY7g8
|
|
||||||
vDQ31pBItyY4pFgPbSpNlbUvUsoPVJ45RasRADDTNCzMzdjFQQXst2V9AoGBAOm7
|
|
||||||
sSpzYfFPLEAhieAkuhtbsX58Boo46djiKVfzGftfp6F9aHTOfzGORU5jrZ16mSbS
|
|
||||||
qkkC6BEFrATX2051dzzXC89fWoJYALrsffE5I3KlKXsCAWSnCP1MMxOfH+Ls61Mr
|
|
||||||
7pK/LKfvJt53mUH4jIdbmmFUDwbg18oBEH+x9PmbAoGAS/+JqXu9N67rIxDGUE6W
|
|
||||||
3tacI0f2+U9Uhe67/DTZaXyc8YFTlXU0uWKIWy+bw5RaYeM9tlL/f/f+m2i25KK+
|
|
||||||
vrZ7zNag7CWU5GJovGyykDnauTpZaYM03mN0VPT08/uc/zXIYqyknbhlIeaZynCK
|
|
||||||
fDB3LUF0NVCknz20WCIGU0kCgYEAkxY0ZXx61Dp4pFr2wwEZxQGs7uXpz64FKyEX
|
|
||||||
12r6nMATY4Lh6y/Px0W6w5vis8lk+5Ny6cNUevHQ0LNuJS+yu6ywl+1vrbrnqroM
|
|
||||||
f3LvpcPeGLSoX8jl1VDQi7aFgG6LoKly1xJLbdsH4NPutB9PgBbbTghx9GgmI88L
|
|
||||||
rPA2M6UCgYBOmkYJocNgxg6B1/n4Tb9fN1Q/XuJrFDE6NxVUoke+IIyMPRH7FC3m
|
|
||||||
VMYzu+b7zTVJjaBb1cmJemxl/xajziWDofJYPefhdbOVU7HXtmJFY0IG3pVxU1zW
|
|
||||||
3bmDj5QAtCUDpuuNa6GEIT0YR4+D/V7o3DmlZ0tVIwKJmVJoQ2f5dw==
|
|
||||||
-----END RSA PRIVATE KEY-----
|
|
||||||
'';
|
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
Loading…
Reference in New Issue