Skip to content

Commit 4c7bd70

Browse files
authored
Merge pull request #1277 from nicholasyang2022/bsc_1217279_20221124
[crmsh-4.5] Fix: bootstrap: fix the owner and permission of file authorized_keys(bsc#1217279)
2 parents 65aa0a0 + 32eccf6 commit 4c7bd70

File tree

5 files changed

+112
-15
lines changed

5 files changed

+112
-15
lines changed

crmsh/bootstrap.py

Lines changed: 18 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -988,8 +988,12 @@ def configure_ssh_key(user):
988988
utils.su_get_stdout_or_raise_error(cmd, user=user)
989989

990990
if not utils.detect_file(authorized_file):
991-
cmd = "touch {}".format(authorized_file)
991+
cmd = "touch {file} && chmod 0600 {file}".format(file=authorized_file)
992992
utils.su_get_stdout_or_raise_error(cmd, user=user)
993+
else:
994+
# bsc#1217279: ~hacluster/.ssh/authorized_keys created by old versions may have root:root 0600
995+
cmd = "chown {user}: {file} && chmod 0600 {file}".format(user=user, file=authorized_file)
996+
utils.get_stdout_or_raise_error(cmd)
993997

994998
append_unique(public_key, authorized_file, user)
995999

@@ -1059,9 +1063,12 @@ def export_ssh_key_non_interactive(local_user_to_export, remote_user_to_swap, re
10591063
with open(os.path.expanduser('~{}/.ssh/id_rsa.pub'.format(local_user_to_export)), 'r', encoding='utf-8') as f:
10601064
public_key = f.read()
10611065
# FIXME: prevent duplicated entries in authorized_keys
1062-
cmd = '''mkdir -p ~{user}/.ssh && chown {user} ~{user}/.ssh && chmod 0700 ~{user}/.ssh && cat >> ~{user}/.ssh/authorized_keys << "EOF"
1066+
cmd = '''mkdir -p ~{user}/.ssh \\
1067+
&& chown {user}: ~{user}/.ssh && chmod 0700 ~{user}/.ssh \\
1068+
&& cat >> ~{user}/.ssh/authorized_keys << "EOF"
10631069
{key}
10641070
EOF
1071+
chown {user}: ~{user}/.ssh/authorized_keys && chmod 0600 ~{user}/.ssh/authorized_keys
10651072
'''.format(user=remote_user_to_swap, key=public_key)
10661073
result = utils.su_subprocess_run(
10671074
local_sudoer,
@@ -1081,7 +1088,9 @@ def import_ssh_key(local_user, remote_user, local_sudoer, remote_node, remote_su
10811088
remote_key_content = remote_public_key_from(remote_user, local_sudoer, remote_node, remote_sudoer)
10821089
_, _, local_authorized_file = key_files(local_user).values()
10831090
if not utils.check_text_included(remote_key_content, local_authorized_file, remote=None):
1084-
cmd = "echo '{}' >> {}".format(remote_key_content, local_authorized_file)
1091+
cmd = """echo '{content}' >> {file}
1092+
chown '{user}': {file} && chmod 0600 {file}
1093+
""".format(content=remote_key_content, file=local_authorized_file, user=local_user)
10851094
utils.get_stdout_or_raise_error(cmd, remote=None)
10861095

10871096
def append_to_remote_file(fromfile, user, remote_node, tofile):
@@ -1724,6 +1733,12 @@ def swap_public_ssh_key(
17241733
if add:
17251734
public_key = generate_ssh_key_pair_on_remote(local_sudoer, remote_node, remote_sudoer, remote_user_to_swap)
17261735
_, _, local_authorized_file = key_files(local_user_to_swap).values()
1736+
# bsc#1217279: ~hacluster/.ssh/authorized_keys created by old versions may have root:root 0600,
1737+
# sed running as hacluster will not be able to read it
1738+
utils.get_stdout_or_raise_error("chown '{user}': '{file}' && chmod 0600 '{file}'".format(
1739+
user=local_user_to_swap,
1740+
file=local_authorized_file,
1741+
))
17271742
utils.su_get_stdout_or_raise_error("sed -i '$a {}' '{}'".format(public_key, local_authorized_file), local_user_to_swap)
17281743
return public_key
17291744
else:

crmsh/prun/prun.py

Lines changed: 21 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -96,29 +96,39 @@ def prun_multimap(
9696

9797

9898
def _build_run_task(remote: str, cmdline: str) -> Task:
99-
local_sudoer, remote_sudoer = crmsh.utils.UserOfHost.instance().user_pair_for_ssh(remote)
10099
if _is_local_host(remote):
101100
if 0 == os.geteuid():
102101
args = ['/bin/sh']
103-
elif local_sudoer == crmsh.userdir.getuser():
104-
args = ['sudo', '/bin/sh']
102+
remote_sudoer = 'root'
105103
else:
106-
raise AssertionError('trying to run sudo as a non-root user')
104+
remote_sudoer = crmsh.userdir.get_sudoer()
105+
if remote_sudoer == crmsh.userdir.getuser():
106+
args = ['sudo', '/bin/sh']
107+
else:
108+
raise AssertionError('trying to run sudo as a non-root user')
109+
return Task(
110+
args,
111+
cmdline.encode('utf-8'),
112+
stdout=Task.Capture,
113+
stderr=Task.Capture,
114+
context={"host": remote, "ssh_user": remote_sudoer},
115+
)
107116
else:
117+
local_sudoer, remote_sudoer = crmsh.utils.UserOfHost.instance().user_pair_for_ssh(remote)
108118
shell = 'ssh {} {}@{} sudo -H /bin/sh'.format(crmsh.constants.SSH_OPTION, remote_sudoer, remote)
109119
if local_sudoer == crmsh.userdir.getuser():
110120
args = ['/bin/sh', '-c', shell]
111121
elif os.geteuid() == 0:
112122
args = ['su', local_sudoer, '--login', '-c', shell]
113123
else:
114124
raise AssertionError('trying to run su as a non-root user')
115-
return Task(
116-
args,
117-
cmdline.encode('utf-8'),
118-
stdout=Task.Capture,
119-
stderr=Task.Capture,
120-
context={"host": remote, "ssh_user": remote_sudoer},
121-
)
125+
return Task(
126+
args,
127+
cmdline.encode('utf-8'),
128+
stdout=Task.Capture,
129+
stderr=Task.Capture,
130+
context={"host": remote, "ssh_user": remote_sudoer},
131+
)
122132

123133

124134
def _handle_run_result(task: Task, interceptor: PRunInterceptor = PRunInterceptor()):

test/features/bootstrap_bugs.feature

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -200,3 +200,38 @@ Feature: Regression test for bootstrap bugs
200200
And Run "mv /root/.config/crm/crm.conf{.bak,}" on "hanode1"
201201
And Run "mv /root/.ssh{,.bak}" on "hanode1"
202202
Then Run "crm status" OK on "hanode1"
203+
And Run "rm -rf /root/.ssh && mv /root/.ssh{.bak,}" OK on "hanode1"
204+
205+
# skip non-root as behave_agent is not able to run commands interactively with non-root sudoer
206+
@skip_non_root
207+
@clean
208+
Scenario: Owner and permssion of file authorized_keys (bsc#1217279)
209+
Given Cluster service is "stopped" on "hanode1"
210+
And Cluster service is "stopped" on "hanode2"
211+
# in a newly created cluster
212+
When Run "crm cluster init -y" on "hanode1"
213+
And Run "crm cluster join -c hanode1 -y" on "hanode2"
214+
Then Run "stat -c '%U:%G' ~hacluster/.ssh/authorized_keys" OK on "hanode1"
215+
And Expected "hacluster:haclient" in stdout
216+
And Run "stat -c '%U:%G' ~hacluster/.ssh/authorized_keys" OK on "hanode2"
217+
And Expected "hacluster:haclient" in stdout
218+
# in an upgraded cluster in which ~hacluster/.ssh/authorized_keys exists
219+
When Run "chown root:root ~hacluster/.ssh/authorized_keys && chmod 0600 ~hacluster/.ssh/authorized_keys" on "hanode1"
220+
And Run "chown root:root ~hacluster/.ssh/authorized_keys && chmod 0600 ~hacluster/.ssh/authorized_keys" on "hanode2"
221+
And Run "rm -f /var/lib/crmsh/upgrade_seq" on "hanode1"
222+
And Run "rm -f /var/lib/crmsh/upgrade_seq" on "hanode2"
223+
And Run "crm status" on "hanode1"
224+
Then Run "stat -c '%U:%G' ~hacluster/.ssh/authorized_keys" OK on "hanode1"
225+
And Expected "hacluster:haclient" in stdout
226+
Then Run "stat -c '%U:%G' ~hacluster/.ssh/authorized_keys" OK on "hanode2"
227+
And Expected "hacluster:haclient" in stdout
228+
# in an upgraded cluster in which ~hacluster/.ssh/authorized_keys does not exist
229+
When Run "rm -rf /var/lib/heartbeat/cores/hacluster/.ssh/" on "hanode1"
230+
And Run "rm -rf /var/lib/heartbeat/cores/hacluster/.ssh/" on "hanode2"
231+
And Run "rm -f /var/lib/crmsh/upgrade_seq" on "hanode1"
232+
And Run "rm -f /var/lib/crmsh/upgrade_seq" on "hanode2"
233+
And Run "crm status" on "hanode1"
234+
Then Run "stat -c '%U:%G' ~hacluster/.ssh/authorized_keys" OK on "hanode1"
235+
And Expected "hacluster:haclient" in stdout
236+
Then Run "stat -c '%U:%G' ~hacluster/.ssh/authorized_keys" OK on "hanode2"
237+
And Expected "hacluster:haclient" in stdout

test/unittests/test_bootstrap.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -533,7 +533,7 @@ def test_configure_ssh_key(self, mock_change_shell, mock_key_files, mock_detect,
533533
mock.call("/test/.ssh/authorized_keys")
534534
])
535535
mock_append_unique.assert_called_once_with("/test/.ssh/id_rsa.pub", "/test/.ssh/authorized_keys", "test")
536-
mock_su.assert_called_once_with('touch /test/.ssh/authorized_keys', user="test")
536+
mock_su.assert_called_once_with('touch /test/.ssh/authorized_keys && chmod 0600 /test/.ssh/authorized_keys', user="test")
537537

538538
@mock.patch('crmsh.bootstrap.append_to_remote_file')
539539
@mock.patch('crmsh.utils.check_file_content_included')

test/unittests/test_prun.py

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -108,6 +108,43 @@ def test_prun_root(
108108
self.assertTrue(isinstance(results, typing.Dict))
109109
self.assertSetEqual({"host1", "host2"}, set(results.keys()))
110110

111+
@mock.patch("os.geteuid")
112+
@mock.patch("crmsh.userdir.getuser")
113+
@mock.patch("crmsh.prun.prun._is_local_host")
114+
@mock.patch("crmsh.utils.UserOfHost.user_pair_for_ssh")
115+
@mock.patch("crmsh.prun.runner.Runner.run")
116+
@mock.patch("crmsh.prun.runner.Runner.add_task")
117+
def test_prun_localhost(
118+
self,
119+
mock_runner_add_task: mock.MagicMock,
120+
mock_runner_run: mock.MagicMock,
121+
mock_user_pair_for_ssh: mock.MagicMock,
122+
mock_is_local_host: mock.MagicMock,
123+
mock_getuser: mock.MagicMock,
124+
mock_geteuid: mock.MagicMock,
125+
):
126+
host_cmdline = {"host1": "foo"}
127+
#mock_user_pair_for_ssh.return_value = "alice", "bob"
128+
mock_is_local_host.return_value = True
129+
mock_getuser.return_value = 'root'
130+
mock_geteuid.return_value = 0
131+
results = crmsh.prun.prun.prun(host_cmdline)
132+
mock_user_pair_for_ssh.assert_not_called()
133+
mock_is_local_host.assert_called_once_with('host1')
134+
mock_runner_add_task.assert_called_once_with(
135+
TaskArgumentsEq(
136+
['/bin/sh'],
137+
b'foo',
138+
stdout=crmsh.prun.runner.Task.Capture,
139+
stderr=crmsh.prun.runner.Task.Capture,
140+
context={"host": 'host1', "ssh_user": 'root'},
141+
)
142+
)
143+
mock_user_pair_for_ssh.assert_not_called()
144+
mock_runner_run.assert_called_once()
145+
self.assertTrue(isinstance(results, typing.Dict))
146+
self.assertSetEqual({"host1"}, set(results.keys()))
147+
111148

112149
class TaskArgumentsEq(crmsh.prun.runner.Task):
113150
def __eq__(self, other):

0 commit comments

Comments
 (0)