nixos/keystone: secrets can be read from files

A secret can be stored in a file. It is written at runtime in the
configuration file.
Note it is also possible to write them in the nix store for dev
purposes.

Changed files
+131 -22
nixos
modules
virtualisation
tests
+54
nixos/modules/virtualisation/openstack/common.nix
···
···
+
{ lib }:
+
+
with lib;
+
+
rec {
+
# A shell script string helper to get the value of a secret at
+
# runtime.
+
getSecret = secretOption:
+
if secretOption.storage == "fromFile"
+
then ''$(cat ${secretOption.value})''
+
else ''${secretOption.value}'';
+
+
+
# A shell script string help to replace at runtime in a file the
+
# pattern of a secret by its value.
+
replaceSecret = secretOption: filename: ''
+
sed -i "s/${secretOption.pattern}/${getSecret secretOption}/g" ${filename}
+
'';
+
+
# This generates an option that can be used to declare secrets which
+
# can be stored in the nix store, or not. A pattern is written in
+
# the nix store to represent the secret. The pattern can
+
# then be overwritten with the value of the secret at runtime.
+
mkSecretOption = {name, description ? ""}:
+
mkOption {
+
description = description;
+
type = types.submodule ({
+
options = {
+
pattern = mkOption {
+
type = types.str;
+
default = "##${name}##";
+
description = "The pattern that represent the secret.";
+
};
+
storage = mkOption {
+
type = types.enum [ "fromNixStore" "fromFile" ];
+
description = ''
+
Choose the way the password is provisionned. If
+
fromNixStore is used, the value is the password and it is
+
written in the nix store. If fromFile is used, the value
+
is a path from where the password will be read at
+
runtime. This is generally used with <link
+
xlink:href="https://nixos.org/nixops/manual/#opt-deployment.keys">
+
deployment keys</link> of Nixops.
+
'';};
+
value = mkOption {
+
type = types.str;
+
description = ''
+
If the storage is fromNixStore, the value is the password itself,
+
otherwise it is a path to the file that contains the password.
+
'';
+
};
+
};});
+
};
+
}
+46 -17
nixos/modules/virtualisation/openstack/keystone.nix
···
{ config, lib, pkgs, ... }:
-
with lib;
let
cfg = config.virtualisation.openstack.keystone;
-
keystoneConf = pkgs.writeText "keystone.conf" ''
[DEFAULT]
-
admin_token = ${cfg.adminToken}
policy_file=${cfg.package}/etc/policy.json
[database]
-
connection = ${cfg.databaseConnection}
[paste_deploy]
config_file = ${cfg.package}/etc/keystone-paste.ini
${cfg.extraConfig}
'';
in {
options.virtualisation.openstack.keystone = {
package = mkOption {
···
'';
};
-
adminToken = mkOption {
-
type = types.str;
-
default = "mySuperToken";
description = ''
This is the admin token used to boostrap keystone,
ie. to provision first resources.
···
'';
};
-
adminPassword = mkOption {
-
type = types.str;
-
default = "admin";
description = ''
The keystone admin user's password.
'';
···
};
};
-
databaseConnection = mkOption {
type = types.str;
-
default = mysql://keystone:keystone@localhost/keystone;
description = ''
-
The SQLAlchemy connection string to use to connect to the
-
Keystone database.
'';
};
};
···
systemd.services.keystone-all = {
description = "OpenStack Keystone Daemon";
-
packages = [ mysql ];
after = [ "network.target"];
path = [ cfg.package pkgs.mysql pkgs.curl pkgs.pythonPackages.keystoneclient pkgs.gawk ];
wantedBy = [ "multi-user.target" ];
preStart = ''
mkdir -m 755 -p /var/lib/keystone
# Initialise the database
${cfg.package}/bin/keystone-manage --config-file=${keystoneConf} db_sync
# Set up the keystone's PKI infrastructure
···
# We use the service token to create a first admin user
export OS_SERVICE_ENDPOINT=http://localhost:35357/v2.0
-
export OS_SERVICE_TOKEN=${cfg.adminToken}
# If the tenant service doesn't exist, we consider
# keystone is not initialized
···
then
keystone tenant-create --name service
keystone tenant-create --name ${cfg.bootstrap.adminTenant}
-
keystone user-create --name ${cfg.bootstrap.adminUsername} --tenant ${cfg.bootstrap.adminTenant} --pass ${cfg.bootstrap.adminPassword}
keystone role-create --name admin
keystone role-create --name Member
keystone user-role-add --tenant ${cfg.bootstrap.adminTenant} --user ${cfg.bootstrap.adminUsername} --role admin
···
{ config, lib, pkgs, ... }:
+
with lib; with import ./common.nix {inherit lib;};
let
cfg = config.virtualisation.openstack.keystone;
+
keystoneConfTpl = pkgs.writeText "keystone.conf" ''
[DEFAULT]
+
admin_token = ${cfg.adminToken.pattern}
policy_file=${cfg.package}/etc/policy.json
[database]
+
+
connection = "mysql://${cfg.database.user}:${cfg.database.password.pattern}@${cfg.database.host}/${cfg.database.name}"
[paste_deploy]
config_file = ${cfg.package}/etc/keystone-paste.ini
${cfg.extraConfig}
'';
+
keystoneConf = "/var/lib/keystone/keystone.conf";
+
in {
options.virtualisation.openstack.keystone = {
package = mkOption {
···
'';
};
+
adminToken = mkSecretOption {
+
name = "adminToken";
description = ''
This is the admin token used to boostrap keystone,
ie. to provision first resources.
···
'';
};
+
adminPassword = mkSecretOption {
+
name = "keystoneAdminPassword";
description = ''
The keystone admin user's password.
'';
···
};
};
+
database = {
+
host = mkOption {
+
type = types.str;
+
default = "localhost";
+
description = ''
+
Host of the database.
+
'';
+
};
+
+
name = mkOption {
+
type = types.str;
+
default = "keystone";
+
description = ''
+
Name of the existing database.
+
'';
+
};
+
+
user = mkOption {
type = types.str;
+
default = "keystone";
description = ''
+
The database user. The user must exist and has access to
+
the specified database.
'';
+
};
+
password = mkSecretOption {
+
name = "mysqlPassword";
+
description = "The database user's password";};
};
};
···
systemd.services.keystone-all = {
description = "OpenStack Keystone Daemon";
after = [ "network.target"];
path = [ cfg.package pkgs.mysql pkgs.curl pkgs.pythonPackages.keystoneclient pkgs.gawk ];
wantedBy = [ "multi-user.target" ];
preStart = ''
mkdir -m 755 -p /var/lib/keystone
+
+
cp ${keystoneConfTpl} ${keystoneConf};
+
chown keystone:keystone ${keystoneConf};
+
chmod 640 ${keystoneConf}
+
+
${replaceSecret cfg.database.password keystoneConf}
+
${replaceSecret cfg.adminToken keystoneConf}
+
# Initialise the database
${cfg.package}/bin/keystone-manage --config-file=${keystoneConf} db_sync
# Set up the keystone's PKI infrastructure
···
# We use the service token to create a first admin user
export OS_SERVICE_ENDPOINT=http://localhost:35357/v2.0
+
export OS_SERVICE_TOKEN=${getSecret cfg.adminToken}
# If the tenant service doesn't exist, we consider
# keystone is not initialized
···
then
keystone tenant-create --name service
keystone tenant-create --name ${cfg.bootstrap.adminTenant}
+
keystone user-create --name ${cfg.bootstrap.adminUsername} --tenant ${cfg.bootstrap.adminTenant} --pass ${getSecret cfg.bootstrap.adminPassword}
keystone role-create --name admin
keystone role-create --name Member
keystone user-role-add --tenant ${cfg.bootstrap.adminTenant} --user ${cfg.bootstrap.adminUsername} --role admin
+31 -5
nixos/tests/keystone.nix
···
with pkgs.lib;
let
createKeystoneDb = pkgs.writeText "create-keystone-db.sql" ''
create database keystone;
-
GRANT ALL PRIVILEGES ON keystone.* TO 'keystone'@'localhost' IDENTIFIED BY 'keystone';
-
GRANT ALL PRIVILEGES ON keystone.* TO 'keystone'@'%' IDENTIFIED BY 'keystone';
'';
# The admin keystone account
-
adminOpenstackCmd = "OS_TENANT_NAME=admin OS_USERNAME=admin OS_PASSWORD=admin OS_AUTH_URL=http://localhost:5000/v3 OS_IDENTITY_API_VERSION=3 openstack";
# The created demo keystone account
demoOpenstackCmd = "OS_TENANT_NAME=demo OS_USERNAME=demo OS_PASSWORD=demo OS_AUTH_URL=http://localhost:5000/v3 OS_IDENTITY_API_VERSION=3 openstack";
···
machine =
{ config, pkgs, ... }:
{
services.mysql.enable = true;
services.mysql.initialScript = createKeystoneDb;
virtualisation = {
-
openstack.keystone.enable = true;
-
openstack.keystone.bootstrap.enable = true;
memorySize = 2096;
diskSize = 4 * 1024;
···
with pkgs.lib;
let
+
keystoneMysqlPassword = "keystoneMysqlPassword";
+
keystoneMysqlPasswordFile = "/var/run/keystoneMysqlPassword";
+
keystoneAdminPassword = "keystoneAdminPassword";
+
createKeystoneDb = pkgs.writeText "create-keystone-db.sql" ''
create database keystone;
+
GRANT ALL PRIVILEGES ON keystone.* TO 'keystone'@'localhost' IDENTIFIED BY '${keystoneMysqlPassword}';
+
GRANT ALL PRIVILEGES ON keystone.* TO 'keystone'@'%' IDENTIFIED BY '${keystoneMysqlPassword}';
'';
# The admin keystone account
+
adminOpenstackCmd = "OS_TENANT_NAME=admin OS_USERNAME=admin OS_PASSWORD=${keystoneAdminPassword} OS_AUTH_URL=http://localhost:5000/v3 OS_IDENTITY_API_VERSION=3 openstack";
# The created demo keystone account
demoOpenstackCmd = "OS_TENANT_NAME=demo OS_USERNAME=demo OS_PASSWORD=demo OS_AUTH_URL=http://localhost:5000/v3 OS_IDENTITY_API_VERSION=3 openstack";
···
machine =
{ config, pkgs, ... }:
{
+
# This is to simulate nixops deployment process.
+
# https://nixos.org/nixops/manual/#opt-deployment.keys
+
boot.postBootCommands = "echo ${keystoneMysqlPassword} > ${keystoneMysqlPasswordFile}";
+
services.mysql.enable = true;
services.mysql.initialScript = createKeystoneDb;
virtualisation = {
+
+
openstack.keystone = {
+
enable = true;
+
# Check if we can get the secret from a file
+
database.password = {
+
value = keystoneMysqlPasswordFile;
+
storage = "fromFile";
+
};
+
adminToken = {
+
value = "adminToken";
+
storage = "fromNixStore";
+
};
+
+
bootstrap.enable = true;
+
# Check if we can get the secret from the store
+
bootstrap.adminPassword = {
+
value = keystoneAdminPassword;
+
storage = "fromNixStore";
+
};
+
};
memorySize = 2096;
diskSize = 4 * 1024;