Skip to content

Commit c538024

Browse files
committed
Fix: bootstrap: setup authorized ssh access for hacluster(CVE-2020-35459, bsc#1179999; CVE-2021-3020, bsc#1180571)
1 parent 8dea33f commit c538024

File tree

2 files changed

+87
-26
lines changed

2 files changed

+87
-26
lines changed

Diff for: crmsh/bootstrap.py

+76-25
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@
3131
from . import clidisplay
3232
from . import term
3333
from . import lock
34+
from . import userdir
3435

3536

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

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

@@ -95,6 +94,7 @@ def __init__(self):
9594
self.args = None
9695
self.ui_context = None
9796
self.interfaces_inst = None
97+
self.with_other_user = True
9898
self.default_nic_list = []
9999
self.default_ip_list = []
100100
self.local_ip_list = []
@@ -1134,22 +1134,66 @@ def init_ssh():
11341134
Configure passwordless SSH.
11351135
"""
11361136
utils.start_service("sshd.service", enable=True)
1137-
configure_local_ssh_key()
1137+
for user in USER_LIST:
1138+
configure_local_ssh_key(user)
11381139

11391140

1140-
def configure_local_ssh_key():
1141+
def key_files(user):
1142+
"""
1143+
Find home directory for user and return key files with abspath
1144+
"""
1145+
keyfile_dict = {}
1146+
home_dir = userdir.gethomedir(user)
1147+
keyfile_dict['private'] = "{}/.ssh/id_rsa".format(home_dir)
1148+
keyfile_dict['public'] = "{}/.ssh/id_rsa.pub".format(home_dir)
1149+
keyfile_dict['authorized'] = "{}/.ssh/authorized_keys".format(home_dir)
1150+
return keyfile_dict
1151+
1152+
1153+
def is_nologin(user):
1154+
"""
1155+
Check if user's shell is /sbin/nologin
1156+
"""
1157+
with open("/etc/passwd") as f:
1158+
return re.search("{}:.*:/sbin/nologin".format(user), f.read())
1159+
1160+
1161+
def change_user_shell(user):
1162+
"""
1163+
To change user's login shell
1164+
"""
1165+
if user != "root" and is_nologin(user):
1166+
if not _context.yes_to_all:
1167+
status("""
1168+
User {} will be changed the login shell as /bin/bash, and
1169+
be setted up authorized ssh access among cluster nodes""".format(user))
1170+
if not confirm("Continue?"):
1171+
_context.with_other_user = False
1172+
return
1173+
invoke("usermod -s /bin/bash {}".format(user))
1174+
1175+
1176+
def configure_local_ssh_key(user="root"):
11411177
"""
11421178
Configure ssh rsa key locally
11431179
1144-
If /root/.ssh/id_rsa not exist, generate a new one
1145-
Add /root/.ssh/id_rsa.pub to /root/.ssh/authorized_keys anyway, make sure itself authorized
1180+
If <home_dir>/.ssh/id_rsa not exist, generate a new one
1181+
Add <home_dir>/.ssh/id_rsa.pub to <home_dir>/.ssh/authorized_keys anyway, make sure itself authorized
11461182
"""
1147-
if not os.path.exists(RSA_PRIVATE_KEY):
1148-
status("Generating SSH key")
1149-
invoke("ssh-keygen -q -f {} -C 'Cluster Internal on {}' -N ''".format(RSA_PRIVATE_KEY, utils.this_node()))
1150-
if not os.path.exists(AUTHORIZED_KEYS_FILE):
1151-
open(AUTHORIZED_KEYS_FILE, 'w').close()
1152-
append_unique(RSA_PUBLIC_KEY, AUTHORIZED_KEYS_FILE)
1183+
change_user_shell(user)
1184+
1185+
private_key, public_key, authorized_file = key_files(user).values()
1186+
if not os.path.exists(private_key):
1187+
status("Generating SSH key for {}".format(user))
1188+
cmd = "ssh-keygen -q -f {} -C 'Cluster Internal on {}' -N ''".format(private_key, utils.this_node())
1189+
cmd = utils.add_su(cmd, user)
1190+
rc, _, err = invoke(cmd)
1191+
if not rc:
1192+
error("Failed to generate ssh key for {}: {}".format(user, err))
1193+
1194+
if not os.path.exists(authorized_file):
1195+
open(authorized_file, 'w').close()
1196+
append_unique(public_key, authorized_file)
11531197

11541198

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

18731917
utils.start_service("sshd.service", enable=True)
1874-
configure_local_ssh_key()
1875-
swap_public_ssh_key(seed_host)
1918+
for user in USER_LIST:
1919+
configure_local_ssh_key(user)
1920+
swap_public_ssh_key(seed_host, user)
18761921

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

18851930

1886-
def swap_public_ssh_key(remote_node):
1931+
def swap_public_ssh_key(remote_node, user="root"):
18871932
"""
18881933
Swap public ssh key between remote_node and local
18891934
"""
1935+
if user != "root" and not _context.with_other_user:
1936+
return
1937+
1938+
_, public_key, authorized_file = key_files(user).values()
18901939
# Detect whether need password to login to remote_node
1891-
if utils.check_ssh_passwd_need(remote_node):
1940+
if utils.check_ssh_passwd_need(remote_node, user):
18921941
# If no passwordless configured, paste /root/.ssh/id_rsa.pub to remote_node's /root/.ssh/authorized_keys
1893-
status("Configuring SSH passwordless with root@{}".format(remote_node))
1942+
status("Configuring SSH passwordless with {}@{}".format(user, remote_node))
18941943
# After this, login to remote_node is passwordless
1895-
append_to_remote_file(RSA_PUBLIC_KEY, remote_node, AUTHORIZED_KEYS_FILE)
1944+
append_to_remote_file(public_key, remote_node, authorized_file)
18961945

18971946
try:
18981947
# Fetch public key file from remote_node
1899-
public_key_file_remote = fetch_public_key_from_remote_node(remote_node)
1948+
public_key_file_remote = fetch_public_key_from_remote_node(remote_node, user)
19001949
except ValueError as err:
19011950
warn(err)
19021951
return
19031952
# Append public key file from remote_node to local's /root/.ssh/authorized_keys
19041953
# After this, login from remote_node is passwordless
19051954
# Should do this step even passwordless is True, to make sure we got two-way passwordless
1906-
append_unique(public_key_file_remote, AUTHORIZED_KEYS_FILE)
1955+
append_unique(public_key_file_remote, authorized_file)
19071956

19081957

1909-
def fetch_public_key_from_remote_node(node):
1958+
def fetch_public_key_from_remote_node(node, user="root"):
19101959
"""
19111960
Fetch public key file from remote node
19121961
Return a temp file contains public key
@@ -1915,8 +1964,9 @@ def fetch_public_key_from_remote_node(node):
19151964

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

21292179
# Swap ssh public key between join node and other cluster nodes
21302180
for node in cluster_nodes_list:
2131-
swap_public_ssh_key(node)
2181+
for user in USER_LIST:
2182+
swap_public_ssh_key(node, user)
21322183

21332184

21342185
def join_cluster(seed_host):
@@ -2487,7 +2538,7 @@ def bootstrap_join(context):
24872538
check_tty()
24882539

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

24932544
if not check_prereqs("join"):

Diff for: crmsh/utils.py

+11-1
Original file line numberDiff line numberDiff line change
@@ -344,6 +344,15 @@ def add_sudo(cmd):
344344
return cmd
345345

346346

347+
def add_su(cmd, user):
348+
"""
349+
Wrapped cmd with su -c "<cmd>" <user>
350+
"""
351+
if user == "root":
352+
return cmd
353+
return "su -c \"{}\" {}".format(cmd, user)
354+
355+
347356
def chown(path, user, group):
348357
if isinstance(user, int):
349358
uid = user
@@ -2084,12 +2093,13 @@ def get_iplist_corosync_using():
20842093
return re.findall(r'id\s*=\s*(.*)', out)
20852094

20862095

2087-
def check_ssh_passwd_need(host):
2096+
def check_ssh_passwd_need(host, user="root"):
20882097
"""
20892098
Check whether access to host need password
20902099
"""
20912100
ssh_options = "-o StrictHostKeyChecking=no -o EscapeChar=none -o ConnectTimeout=15"
20922101
ssh_cmd = "ssh {} -T -o Batchmode=yes {} true".format(ssh_options, host)
2102+
ssh_cmd = add_su(ssh_cmd, user)
20932103
rc, _, _ = get_stdout_stderr(ssh_cmd)
20942104
return rc != 0
20952105

0 commit comments

Comments
 (0)