Skip to content
Permalink
Browse files Browse the repository at this point in the history
Fix: bootstrap: setup authorized ssh access for hacluster(CVE-2020-35459
, bsc#1179999; CVE-2021-3020, bsc#1180571)
  • Loading branch information
liangxin1300 committed Feb 19, 2021
1 parent 8dea33f commit c538024
Show file tree
Hide file tree
Showing 2 changed files with 87 additions and 26 deletions.
101 changes: 76 additions & 25 deletions crmsh/bootstrap.py
Expand Up @@ -31,6 +31,7 @@
from . import clidisplay
from . import term
from . import lock
from . import userdir


LOG_FILE = "/var/log/crmsh/ha-cluster-bootstrap.log"
Expand All @@ -42,10 +43,8 @@
SYSCONFIG_FW_CLUSTER = "/etc/sysconfig/SuSEfirewall2.d/services/cluster"
PCMK_REMOTE_AUTH = "/etc/pacemaker/authkey"
COROSYNC_CONF_ORIG = tmpfiles.create()[1]
RSA_PRIVATE_KEY = "/root/.ssh/id_rsa"
RSA_PUBLIC_KEY = "/root/.ssh/id_rsa.pub"
AUTHORIZED_KEYS_FILE = "/root/.ssh/authorized_keys"
SERVICES_STOP_LIST = ["corosync-qdevice.service", "corosync.service", "hawk.service"]
USER_LIST = ["root", "hacluster"]

INIT_STAGES = ("ssh", "ssh_remote", "csync2", "csync2_remote", "corosync", "storage", "sbd", "cluster", "vgfs", "admin", "qdevice")

Expand Down Expand Up @@ -95,6 +94,7 @@ def __init__(self):
self.args = None
self.ui_context = None
self.interfaces_inst = None
self.with_other_user = True
self.default_nic_list = []
self.default_ip_list = []
self.local_ip_list = []
Expand Down Expand Up @@ -1134,22 +1134,66 @@ def init_ssh():
Configure passwordless SSH.
"""
utils.start_service("sshd.service", enable=True)
configure_local_ssh_key()
for user in USER_LIST:
configure_local_ssh_key(user)


def configure_local_ssh_key():
def key_files(user):
"""
Find home directory for user and return key files with abspath
"""
keyfile_dict = {}
home_dir = userdir.gethomedir(user)
keyfile_dict['private'] = "{}/.ssh/id_rsa".format(home_dir)
keyfile_dict['public'] = "{}/.ssh/id_rsa.pub".format(home_dir)
keyfile_dict['authorized'] = "{}/.ssh/authorized_keys".format(home_dir)
return keyfile_dict


def is_nologin(user):
"""
Check if user's shell is /sbin/nologin
"""
with open("/etc/passwd") as f:
return re.search("{}:.*:/sbin/nologin".format(user), f.read())


def change_user_shell(user):
"""
To change user's login shell
"""
if user != "root" and is_nologin(user):
if not _context.yes_to_all:
status("""
User {} will be changed the login shell as /bin/bash, and
be setted up authorized ssh access among cluster nodes""".format(user))
if not confirm("Continue?"):
_context.with_other_user = False
return
invoke("usermod -s /bin/bash {}".format(user))


def configure_local_ssh_key(user="root"):
"""
Configure ssh rsa key locally
If /root/.ssh/id_rsa not exist, generate a new one
Add /root/.ssh/id_rsa.pub to /root/.ssh/authorized_keys anyway, make sure itself authorized
If <home_dir>/.ssh/id_rsa not exist, generate a new one
Add <home_dir>/.ssh/id_rsa.pub to <home_dir>/.ssh/authorized_keys anyway, make sure itself authorized
"""
if not os.path.exists(RSA_PRIVATE_KEY):
status("Generating SSH key")
invoke("ssh-keygen -q -f {} -C 'Cluster Internal on {}' -N ''".format(RSA_PRIVATE_KEY, utils.this_node()))
if not os.path.exists(AUTHORIZED_KEYS_FILE):
open(AUTHORIZED_KEYS_FILE, 'w').close()
append_unique(RSA_PUBLIC_KEY, AUTHORIZED_KEYS_FILE)
change_user_shell(user)

private_key, public_key, authorized_file = key_files(user).values()
if not os.path.exists(private_key):
status("Generating SSH key for {}".format(user))
cmd = "ssh-keygen -q -f {} -C 'Cluster Internal on {}' -N ''".format(private_key, utils.this_node())
cmd = utils.add_su(cmd, user)
rc, _, err = invoke(cmd)
if not rc:
error("Failed to generate ssh key for {}: {}".format(user, err))

if not os.path.exists(authorized_file):
open(authorized_file, 'w').close()
append_unique(public_key, authorized_file)


def init_ssh_remote():
Expand Down Expand Up @@ -1871,8 +1915,9 @@ def join_ssh(seed_host):
error("No existing IP/hostname specified (use -c option)")

utils.start_service("sshd.service", enable=True)
configure_local_ssh_key()
swap_public_ssh_key(seed_host)
for user in USER_LIST:
configure_local_ssh_key(user)
swap_public_ssh_key(seed_host, user)

# This makes sure the seed host has its own SSH keys in its own
# authorized_keys file (again, to help with the case where the
Expand All @@ -1883,30 +1928,34 @@ def join_ssh(seed_host):
error("Can't invoke crm cluster init -i {} ssh_remote on {}: {}".format(_context.default_nic_list[0], seed_host, err))


def swap_public_ssh_key(remote_node):
def swap_public_ssh_key(remote_node, user="root"):
"""
Swap public ssh key between remote_node and local
"""
if user != "root" and not _context.with_other_user:
return

_, public_key, authorized_file = key_files(user).values()
# Detect whether need password to login to remote_node
if utils.check_ssh_passwd_need(remote_node):
if utils.check_ssh_passwd_need(remote_node, user):
# If no passwordless configured, paste /root/.ssh/id_rsa.pub to remote_node's /root/.ssh/authorized_keys
status("Configuring SSH passwordless with root@{}".format(remote_node))
status("Configuring SSH passwordless with {}@{}".format(user, remote_node))
# After this, login to remote_node is passwordless
append_to_remote_file(RSA_PUBLIC_KEY, remote_node, AUTHORIZED_KEYS_FILE)
append_to_remote_file(public_key, remote_node, authorized_file)

try:
# Fetch public key file from remote_node
public_key_file_remote = fetch_public_key_from_remote_node(remote_node)
public_key_file_remote = fetch_public_key_from_remote_node(remote_node, user)
except ValueError as err:
warn(err)
return
# Append public key file from remote_node to local's /root/.ssh/authorized_keys
# After this, login from remote_node is passwordless
# Should do this step even passwordless is True, to make sure we got two-way passwordless
append_unique(public_key_file_remote, AUTHORIZED_KEYS_FILE)
append_unique(public_key_file_remote, authorized_file)


def fetch_public_key_from_remote_node(node):
def fetch_public_key_from_remote_node(node, user="root"):
"""
Fetch public key file from remote node
Return a temp file contains public key
Expand All @@ -1915,8 +1964,9 @@ def fetch_public_key_from_remote_node(node):

# For dsa, might need to add PubkeyAcceptedKeyTypes=+ssh-dss to config file, see
# https://superuser.com/questions/1016989/ssh-dsa-keys-no-longer-work-for-password-less-authentication
home_dir = userdir.gethomedir(user)
for key in ("id_rsa", "id_ecdsa", "id_ed25519", "id_dsa"):
public_key_file = "/root/.ssh/{}.pub".format(key)
public_key_file = "{}/.ssh/{}.pub".format(home_dir, key)
cmd = "ssh -oStrictHostKeyChecking=no root@{} 'test -f {}'".format(node, public_key_file)
if not invokerc(cmd):
continue
Expand Down Expand Up @@ -2128,7 +2178,8 @@ def setup_passwordless_with_other_nodes(init_node):

# Swap ssh public key between join node and other cluster nodes
for node in cluster_nodes_list:
swap_public_ssh_key(node)
for user in USER_LIST:
swap_public_ssh_key(node, user)


def join_cluster(seed_host):
Expand Down Expand Up @@ -2487,7 +2538,7 @@ def bootstrap_join(context):
check_tty()

corosync_active = utils.service_is_active("corosync.service")
if corosync_active:
if corosync_active and _context.stage != "ssh":
error("Abort: Cluster is currently active. Run this command on a node joining the cluster.")

if not check_prereqs("join"):
Expand Down
12 changes: 11 additions & 1 deletion crmsh/utils.py
Expand Up @@ -344,6 +344,15 @@ def add_sudo(cmd):
return cmd


def add_su(cmd, user):
"""
Wrapped cmd with su -c "<cmd>" <user>
"""
if user == "root":
return cmd
return "su -c \"{}\" {}".format(cmd, user)


def chown(path, user, group):
if isinstance(user, int):
uid = user
Expand Down Expand Up @@ -2084,12 +2093,13 @@ def get_iplist_corosync_using():
return re.findall(r'id\s*=\s*(.*)', out)


def check_ssh_passwd_need(host):
def check_ssh_passwd_need(host, user="root"):
"""
Check whether access to host need password
"""
ssh_options = "-o StrictHostKeyChecking=no -o EscapeChar=none -o ConnectTimeout=15"
ssh_cmd = "ssh {} -T -o Batchmode=yes {} true".format(ssh_options, host)
ssh_cmd = add_su(ssh_cmd, user)
rc, _, _ = get_stdout_stderr(ssh_cmd)
return rc != 0

Expand Down

1 comment on commit c538024

@Mingli-Yu
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Does CVE-2021-3020 affect crmsh 4.2.0?

Please sign in to comment.