diff --git a/files/mysql_ed25519_password.py b/files/mysql_ed25519_password.py new file mode 100755 index 000000000..47e646d1f --- /dev/null +++ b/files/mysql_ed25519_password.py @@ -0,0 +1,42 @@ +#!/usr/bin/env python3 +import hashlib +import base64 +import sys + +from nacl.bindings.crypto_scalarmult import \ + crypto_scalarmult_ed25519_base_noclamp + +# https://github.com/MariaDB/server/blob/10.4/plugin/auth_ed25519/ref10/sign.c +# mariadb's use of ed25519: +# . password is the secret seed +# . ed25519's public key (computed from password) is what is stored in mariadb +# . the hash in mariadb is the base64 encoding of the pk minus the last '=' + + +def _scalar_clamp(s32): + ba = bytearray(s32) + ba0 = bytes(bytearray([ba[0] & 248])) + ba31 = bytes(bytearray([(ba[31] & 127) | 64])) + return ba0 + bytes(s32[1:31]) + ba31 + + +def mysql_ed25519_password(pwd): + # h = SHA512(password) + h = hashlib.sha512(pwd).digest() + # s = prune(first_half(h)) + s = _scalar_clamp(h[:32]) + # A = encoded point [s]B + A = crypto_scalarmult_ed25519_base_noclamp(s) + # encoded pk + encoded = base64.b64encode(A)[:-1] + return encoded + + +if __name__ == "__main__": + if len(sys.argv) <= 1: + print("Usage: %s PASSWORD" % sys.argv[0], file=sys.stderr) + sys.exit(1) + else: + pwd = sys.argv[1].encode() + res = mysql_ed25519_password(pwd) + print(res.decode(), end='') diff --git a/lib/puppet/functions/mysql_ed25519_password.rb b/lib/puppet/functions/mysql_ed25519_password.rb new file mode 100644 index 000000000..6967ff462 --- /dev/null +++ b/lib/puppet/functions/mysql_ed25519_password.rb @@ -0,0 +1,19 @@ +# Custom function to generate password hash for MariaDB's auth_ed25519 +# Input is a regular mariadb user password +# Output is the hashed password as expected by auth_ed25519 +Puppet::Functions.create_function(:'mysql_ed25519_password') do + dispatch :mysql_ed25519_password do + param 'String', :password + return_type 'String' + end + + def mysql_ed25519_password(password) + # mysql's auth_ed25519 consists in generating a ed25519 public key + # out of the sha512(password). Unfortunately, there is no native + # ruby implementation of ed25519's unclamped scalar multiplication + # just yet, so rely on an binary to get the hash for now. + hashed = `/etc/puppet/modules/tripleo/files/mysql_ed25519_password.py #{password}` + raise Puppet::Error, 'generated hash is not 43 bytes long.' unless hashed.length == 43 + return hashed + end +end diff --git a/manifests/profile/base/database/mysql.pp b/manifests/profile/base/database/mysql.pp index c9ea9351c..309206b40 100644 --- a/manifests/profile/base/database/mysql.pp +++ b/manifests/profile/base/database/mysql.pp @@ -87,6 +87,11 @@ # (Optional) Maximum number of connections to MySQL. # Defaults to hiera('mysql_max_connections', undef) # +# [*mysql_auth_ed25519*] +# (Optional) Use MariaDB's ed25519 authentication plugin to authenticate +# a user when connecting to the server +# Defaults to hiera('mysql_auth_ed25519', false) +# # [*remove_default_accounts*] # (Optional) Whether or not remove default MySQL accounts. # Defaults to true @@ -112,6 +117,7 @@ class tripleo::profile::base::database::mysql ( $manage_resources = true, $mysql_server_options = {}, $mysql_max_connections = hiera('mysql_max_connections', undef), + $mysql_auth_ed25519 = hiera('mysql_auth_ed25519', false), $remove_default_accounts = true, $step = Integer(hiera('step')), ) { @@ -174,6 +180,7 @@ class tripleo::profile::base::database::mysql ( 'ssl-cert' => $tls_certfile, 'ssl-cipher' => $tls_cipher_list, 'ssl-ca' => undef, + 'plugin_load_add' => 'auth_ed25519', } } $mysql_server_options_real = deep_merge($mysql_server_default, $mysql_server_options) @@ -214,78 +221,88 @@ class tripleo::profile::base::database::mysql ( password_hash => mysql_password(hiera('mysql::server::root_password')), } } + if ($mysql_auth_ed25519) { + ['root@localhost', 'root@%'].each |$user| { + Mysql_user<| title == $user |> { + plugin => 'ed25519', + password_hash => mysql_ed25519_password(hiera('mysql::server::root_password')) + } + } + } + # Note: use 'include_and_check_auth' below rather than 'include' + # to support ed25519 authentication if hiera('aodh_api_enabled', false) { - include aodh::db::mysql + tripleo::profile::base::database::mysql::include_and_check_auth{'aodh::db::mysql':} } if hiera('ceilometer_collector_enabled', false) { - include ceilometer::db::mysql + tripleo::profile::base::database::mysql::include_and_check_auth{'ceilometer::db::mysql':} } if hiera('cinder_api_enabled', false) { - include cinder::db::mysql + tripleo::profile::base::database::mysql::include_and_check_auth{'cinder::db::mysql':} } if hiera('barbican_api_enabled', false) { - include barbican::db::mysql + tripleo::profile::base::database::mysql::include_and_check_auth{'barbican::db::mysql':} } if hiera('designate_api_enabled', false) { - include designate::db::mysql + tripleo::profile::base::database::mysql::include_and_check_auth{'designate::db::mysql':} } if hiera('glance_api_enabled', false) { - include glance::db::mysql + tripleo::profile::base::database::mysql::include_and_check_auth{'glance::db::mysql':} } if hiera('gnocchi_api_enabled', false) { - include gnocchi::db::mysql + tripleo::profile::base::database::mysql::include_and_check_auth{'gnocchi::db::mysql':} } if hiera('heat_engine_enabled', false) { - include heat::db::mysql + tripleo::profile::base::database::mysql::include_and_check_auth{'heat::db::mysql':} } if hiera('ironic_api_enabled', false) { - include ironic::db::mysql + tripleo::profile::base::database::mysql::include_and_check_auth{'ironic::db::mysql':} } if hiera('ironic_inspector_enabled', false) { - include ironic::inspector::db::mysql + tripleo::profile::base::database::mysql::include_and_check_auth{'ironic::inspector::db::mysql':} } if hiera('keystone_enabled', false) { - include keystone::db::mysql + tripleo::profile::base::database::mysql::include_and_check_auth{'keystone::db::mysql':} } if hiera('manila_api_enabled', false) { - include manila::db::mysql + tripleo::profile::base::database::mysql::include_and_check_auth{'manila::db::mysql':} } if hiera('mistral_api_enabled', false) { - include mistral::db::mysql + tripleo::profile::base::database::mysql::include_and_check_auth{'mistral::db::mysql':} } if hiera('neutron_api_enabled', false) { - include neutron::db::mysql + tripleo::profile::base::database::mysql::include_and_check_auth{'neutron::db::mysql':} + } + if hiera('nova_conductor_enabled', false) { + tripleo::profile::base::database::mysql::include_and_check_auth{'nova::db::mysql':} } -    if hiera('nova_conductor_enabled', false) { -      include nova::db::mysql -    } if hiera('nova_api_enabled', false) { - include nova::db::mysql_api + tripleo::profile::base::database::mysql::include_and_check_auth{'nova::db::mysql_api':} } if hiera('placement_enabled', false) { - include placement::db::mysql + tripleo::profile::base::database::mysql::include_and_check_auth{'placement::db::mysql':} } if hiera('octavia_api_enabled', false) { - include octavia::db::mysql + tripleo::profile::base::database::mysql::include_and_check_auth{'octavia::db::mysql':} } if hiera('sahara_api_enabled', false) { - include sahara::db::mysql + tripleo::profile::base::database::mysql::include_and_check_auth{'sahara::db::mysql':} } if hiera('trove_api_enabled', false) { - include trove::db::mysql + tripleo::profile::base::database::mysql::include_and_check_auth{'trove::db::mysql':} } if hiera('panko_api_enabled', false) { - include panko::db::mysql + tripleo::profile::base::database::mysql::include_and_check_auth{'panko::db::mysql':} } if hiera('ec2_api_enabled', false) { - include ec2api::db::mysql + tripleo::profile::base::database::mysql::include_and_check_auth{'ec2api::db::mysql':} } if hiera('zaqar_api_enabled', false) and hiera('zaqar::db::mysql::user', '') == 'zaqar' { # NOTE: by default zaqar uses sqlalchemy - include zaqar::db::mysql + tripleo::profile::base::database::mysql::include_and_check_auth{'zaqar::db::mysql':} } if hiera('veritas_hyperscale_controller_enabled', false) { - include veritas_hyperscale::db::mysql + tripleo::profile::base::database::mysql::include_and_check_auth{'veritas_hyperscale::db::mysql':} } } diff --git a/manifests/profile/base/database/mysql/include_and_check_auth.pp b/manifests/profile/base/database/mysql/include_and_check_auth.pp new file mode 100644 index 000000000..e330eeca0 --- /dev/null +++ b/manifests/profile/base/database/mysql/include_and_check_auth.pp @@ -0,0 +1,49 @@ +# Copyright 2016 Red Hat, Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. +# +# == Class: include_and_check_auth +# +# Include an OpenStack MySQL profile and configures it for alternative +# client authentication like e.g. ed25519 +# +# === Parameters +# +# [*module*] +# (Optional) The puppet module to include +# Defaults to $title +# +# [*mysql_auth_ed25519*] +# (Optional) Use MariaDB's ed25519 authentication plugin to authenticate +# a user when connecting to the server +# Defaults to hiera('mysql_auth_ed25519', false) +# +define tripleo::profile::base::database::mysql::include_and_check_auth( + $module = $title, + $mysql_auth_ed25519 = hiera('mysql_auth_ed25519', false), +) { + include $module + if ($mysql_auth_ed25519) { + # currently all openstack puppet modules create MySQL users + # by hashing their password for the default auth method. + # If ed25519 auth is enabled, we must hash the password + # differently; so do it with a collector until all + # openstack modules support ed25519 auth natively. + $stripped_module_name = regsubst($module,'^::','') + $password_key = "${stripped_module_name}::password" + Openstacklib::Db::Mysql<| tag == $stripped_module_name |> { + plugin => 'ed25519', + password_hash => mysql_ed25519_password(hiera($password_key)) + } + } +} diff --git a/manifests/profile/pacemaker/database/mysql_bundle.pp b/manifests/profile/pacemaker/database/mysql_bundle.pp index 1e32544c2..926711b6b 100644 --- a/manifests/profile/pacemaker/database/mysql_bundle.pp +++ b/manifests/profile/pacemaker/database/mysql_bundle.pp @@ -105,6 +105,11 @@ # Should be an hash. # Defaults to hiera('tripleo::profile::base::database::mysql::mysql_server_options', {} # +# [*mysql_auth_ed25519*] +# (Optional) Use MariaDB's ed25519 authentication plugin to authenticate +# a user when connecting to the server +# Defaults to hiera('mysql_auth_ed25519', false) +# # [*pcs_tries*] # (Optional) The number of times pcs commands should be retried. # Defaults to hiera('pcs_tries', 20) @@ -149,6 +154,7 @@ class tripleo::profile::pacemaker::database::mysql_bundle ( $sst_tls_options = undef, $ipv6 = str2bool(hiera('mysql_ipv6', false)), $mysql_server_options = hiera('tripleo::profile::base::database::mysql::mysql_server_options', {}), + $mysql_auth_ed25519 = hiera('mysql_auth_ed25519', false), $container_backend = 'docker', $log_driver = undef, $tls_priorities = hiera('tripleo::pacemaker::tls_priorities', undef), @@ -498,6 +504,23 @@ MYSQL_HOST=localhost\n", password_hash => mysql_password($mysql_root_password), } + # declare the clustercheck user resource to configure + # ed25519 authentication on stack creation or update. + if ($mysql_auth_ed25519) { + $clustercheck_resource_config = { + plugin => 'ed25519', + password_hash => mysql_ed25519_password($clustercheck_password), + } + } else { + $clustercheck_resource_config = { + password_hash => mysql_password($clustercheck_password), + } + } + mysql_user { 'clustercheck@localhost': + ensure => present, + * => $clustercheck_resource_config, + } + # We create databases and users for services at step 2 as well. This ensures # Galera is up and ready before those get created File['/root/.my.cnf'] -> Mysql_database<||>