Revision 45bc4635
b/Makefile.am | ||
---|---|---|
395 | 395 |
doc/design-partitioned.rst \ |
396 | 396 |
doc/design-query-splitting.rst \ |
397 | 397 |
doc/design-query2.rst \ |
398 |
doc/design-remote-commands.rst \ |
|
399 | 398 |
doc/design-resource-model.rst \ |
399 |
doc/design-restricted-commands.rst \ |
|
400 | 400 |
doc/design-shared-storage.rst \ |
401 | 401 |
doc/design-monitoring-agent.rst \ |
402 | 402 |
doc/design-virtual-clusters.rst \ |
b/doc/design-2.7.rst | ||
---|---|---|
6 | 6 |
|
7 | 7 |
- :doc:`design-bulk-create` |
8 | 8 |
- :doc:`design-opportunistic-locking` |
9 |
- :doc:`design-remote-commands`
|
|
9 |
- :doc:`design-restricted-commands`
|
|
10 | 10 |
- :doc:`design-node-add` |
11 | 11 |
- :doc:`design-virtual-clusters` |
12 | 12 |
- :doc:`design-network` |
/dev/null | ||
---|---|---|
1 |
Design for executing commands via RPC |
|
2 |
===================================== |
|
3 |
|
|
4 |
.. contents:: :depth: 3 |
|
5 |
|
|
6 |
|
|
7 |
Current state and shortcomings |
|
8 |
------------------------------ |
|
9 |
|
|
10 |
We have encountered situations where a node was no longer responding to |
|
11 |
attempts at connecting via SSH or SSH became unavailable through other |
|
12 |
means. Quite often the node daemon is still available, even in |
|
13 |
situations where there's little free memory. The latter is due to the |
|
14 |
node daemon being locked into main memory using ``mlock(2)``. |
|
15 |
|
|
16 |
Since the node daemon does not allow the execution of arbitrary |
|
17 |
commands, quite often the only solution left was either to attempt a |
|
18 |
powercycle request via said node daemon or to physically reset the node. |
|
19 |
|
|
20 |
|
|
21 |
Proposed changes |
|
22 |
---------------- |
|
23 |
|
|
24 |
The goal of this design is to allow the execution of non-arbitrary |
|
25 |
commands via RPC requests. Since this can be dangerous in case the |
|
26 |
cluster certificate (``server.pem``) is leaked, some precautions need to |
|
27 |
be taken: |
|
28 |
|
|
29 |
- No parameters may be passed |
|
30 |
- No absolute or relative path may be passed, only a filename |
|
31 |
- Executable must reside in ``/etc/ganeti/remote-commands``, which must |
|
32 |
be owned by root:root and have mode 0755 or stricter |
|
33 |
- Must be regular files or symlinks |
|
34 |
- Must be executable by root:root |
|
35 |
|
|
36 |
There shall be no way to list available commands or to retrieve an |
|
37 |
executable's contents. The result from a request to execute a specific |
|
38 |
command will either be its output and exit code, or a generic error |
|
39 |
message. Only the receiving node's log files shall contain information |
|
40 |
as to why executing the command failed. |
|
41 |
|
|
42 |
To slow down dictionary attacks on command names in case an attacker |
|
43 |
manages to obtain a copy of ``server.pem``, a system-wide, file-based |
|
44 |
lock is acquired before verifying the command name and its executable. |
|
45 |
If a command can not be executed for some reason, the lock is only |
|
46 |
released with a delay of several seconds, after which the generic error |
|
47 |
message will be returned to the caller. |
|
48 |
|
|
49 |
At first, remote commands will not be made available through the |
|
50 |
:doc:`remote API <rapi>`, though that could be done at a later point |
|
51 |
(with a separate password). |
|
52 |
|
|
53 |
On the command line, a new sub-command will be added to the ``gnt-node`` |
|
54 |
script. |
|
55 |
|
|
56 |
.. vim: set textwidth=72 : |
|
57 |
.. Local Variables: |
|
58 |
.. mode: rst |
|
59 |
.. fill-column: 72 |
|
60 |
.. End: |
b/doc/design-restricted-commands.rst | ||
---|---|---|
1 |
Design for executing commands via RPC |
|
2 |
===================================== |
|
3 |
|
|
4 |
.. contents:: :depth: 3 |
|
5 |
|
|
6 |
|
|
7 |
Current state and shortcomings |
|
8 |
------------------------------ |
|
9 |
|
|
10 |
We have encountered situations where a node was no longer responding to |
|
11 |
attempts at connecting via SSH or SSH became unavailable through other |
|
12 |
means. Quite often the node daemon is still available, even in |
|
13 |
situations where there's little free memory. The latter is due to the |
|
14 |
node daemon being locked into main memory using ``mlock(2)``. |
|
15 |
|
|
16 |
Since the node daemon does not allow the execution of arbitrary |
|
17 |
commands, quite often the only solution left was either to attempt a |
|
18 |
powercycle request via said node daemon or to physically reset the node. |
|
19 |
|
|
20 |
|
|
21 |
Proposed changes |
|
22 |
---------------- |
|
23 |
|
|
24 |
The goal of this design is to allow the execution of non-arbitrary |
|
25 |
commands via RPC requests. Since this can be dangerous in case the |
|
26 |
cluster certificate (``server.pem``) is leaked, some precautions need to |
|
27 |
be taken: |
|
28 |
|
|
29 |
- No parameters may be passed |
|
30 |
- No absolute or relative path may be passed, only a filename |
|
31 |
- Executable must reside in ``/etc/ganeti/restricted-commands``, which must |
|
32 |
be owned by root:root and have mode 0755 or stricter |
|
33 |
- Must be regular files or symlinks |
|
34 |
- Must be executable by root:root |
|
35 |
|
|
36 |
There shall be no way to list available commands or to retrieve an |
|
37 |
executable's contents. The result from a request to execute a specific |
|
38 |
command will either be its output and exit code, or a generic error |
|
39 |
message. Only the receiving node's log files shall contain information |
|
40 |
as to why executing the command failed. |
|
41 |
|
|
42 |
To slow down dictionary attacks on command names in case an attacker |
|
43 |
manages to obtain a copy of ``server.pem``, a system-wide, file-based |
|
44 |
lock is acquired before verifying the command name and its executable. |
|
45 |
If a command can not be executed for some reason, the lock is only |
|
46 |
released with a delay of several seconds, after which the generic error |
|
47 |
message will be returned to the caller. |
|
48 |
|
|
49 |
At first, restricted commands will not be made available through the |
|
50 |
:doc:`remote API <rapi>`, though that could be done at a later point |
|
51 |
(with a separate password). |
|
52 |
|
|
53 |
On the command line, a new sub-command will be added to the ``gnt-node`` |
|
54 |
script. |
|
55 |
|
|
56 |
.. vim: set textwidth=72 : |
|
57 |
.. Local Variables: |
|
58 |
.. mode: rst |
|
59 |
.. fill-column: 72 |
|
60 |
.. End: |
b/doc/index.rst | ||
---|---|---|
51 | 51 |
design-opportunistic-locking.rst |
52 | 52 |
design-ovf-support.rst |
53 | 53 |
design-query2.rst |
54 |
design-remote-commands.rst
|
|
54 |
design-restricted-commands.rst
|
|
55 | 55 |
design-shared-storage.rst |
56 | 56 |
design-virtual-clusters.rst |
57 | 57 |
design-network.rst |
b/lib/backend.py | ||
---|---|---|
1 | 1 |
# |
2 | 2 |
# |
3 | 3 |
|
4 |
# Copyright (C) 2006, 2007, 2008, 2009, 2010, 2011, 2012 Google Inc. |
|
4 |
# Copyright (C) 2006, 2007, 2008, 2009, 2010, 2011, 2012, 2013 Google Inc.
|
|
5 | 5 |
# |
6 | 6 |
# This program is free software; you can redistribute it and/or modify |
7 | 7 |
# it under the terms of the GNU General Public License as published by |
... | ... | |
88 | 88 |
_MASTER_START = "start" |
89 | 89 |
_MASTER_STOP = "stop" |
90 | 90 |
|
91 |
#: Maximum file permissions for remote command directory and executables
|
|
91 |
#: Maximum file permissions for restricted command directory and executables
|
|
92 | 92 |
_RCMD_MAX_MODE = (stat.S_IRWXU | |
93 | 93 |
stat.S_IRGRP | stat.S_IXGRP | |
94 | 94 |
stat.S_IROTH | stat.S_IXOTH) |
95 | 95 |
|
96 |
#: Delay before returning an error for remote commands
|
|
96 |
#: Delay before returning an error for restricted commands
|
|
97 | 97 |
_RCMD_INVALID_DELAY = 10 |
98 | 98 |
|
99 |
#: How long to wait to acquire lock for remote commands (shorter than
|
|
99 |
#: How long to wait to acquire lock for restricted commands (shorter than
|
|
100 | 100 |
#: L{_RCMD_INVALID_DELAY}) to reduce blockage of noded forks when many |
101 | 101 |
#: command requests arrive |
102 | 102 |
_RCMD_LOCK_TIMEOUT = _RCMD_INVALID_DELAY * 0.8 |
... | ... | |
3672 | 3672 |
|
3673 | 3673 |
|
3674 | 3674 |
def _VerifyRestrictedCmdName(cmd): |
3675 |
"""Verifies a remote command name.
|
|
3675 |
"""Verifies a restricted command name.
|
|
3676 | 3676 |
|
3677 | 3677 |
@type cmd: string |
3678 | 3678 |
@param cmd: Command name |
... | ... | |
3694 | 3694 |
|
3695 | 3695 |
|
3696 | 3696 |
def _CommonRestrictedCmdCheck(path, owner): |
3697 |
"""Common checks for remote command file system directories and files.
|
|
3697 |
"""Common checks for restricted command file system directories and files.
|
|
3698 | 3698 |
|
3699 | 3699 |
@type path: string |
3700 | 3700 |
@param path: Path to check |
... | ... | |
3724 | 3724 |
|
3725 | 3725 |
|
3726 | 3726 |
def _VerifyRestrictedCmdDirectory(path, _owner=None): |
3727 |
"""Verifies remote command directory.
|
|
3727 |
"""Verifies restricted command directory.
|
|
3728 | 3728 |
|
3729 | 3729 |
@type path: string |
3730 | 3730 |
@param path: Path to check |
... | ... | |
3745 | 3745 |
|
3746 | 3746 |
|
3747 | 3747 |
def _VerifyRestrictedCmd(path, cmd, _owner=None): |
3748 |
"""Verifies a whole remote command and returns its executable filename.
|
|
3748 |
"""Verifies a whole restricted command and returns its executable filename.
|
|
3749 | 3749 |
|
3750 | 3750 |
@type path: string |
3751 |
@param path: Directory containing remote commands
|
|
3751 |
@param path: Directory containing restricted commands
|
|
3752 | 3752 |
@type cmd: string |
3753 | 3753 |
@param cmd: Command name |
3754 | 3754 |
@rtype: tuple; (boolean, string) |
... | ... | |
3774 | 3774 |
_verify_dir=_VerifyRestrictedCmdDirectory, |
3775 | 3775 |
_verify_name=_VerifyRestrictedCmdName, |
3776 | 3776 |
_verify_cmd=_VerifyRestrictedCmd): |
3777 |
"""Performs a number of tests on a remote command.
|
|
3777 |
"""Performs a number of tests on a restricted command.
|
|
3778 | 3778 |
|
3779 | 3779 |
@type path: string |
3780 |
@param path: Directory containing remote commands
|
|
3780 |
@param path: Directory containing restricted commands
|
|
3781 | 3781 |
@type cmd: string |
3782 | 3782 |
@param cmd: Command name |
3783 | 3783 |
@return: Same as L{_VerifyRestrictedCmd} |
... | ... | |
3804 | 3804 |
_prepare_fn=_PrepareRestrictedCmd, |
3805 | 3805 |
_runcmd_fn=utils.RunCmd, |
3806 | 3806 |
_enabled=constants.ENABLE_RESTRICTED_COMMANDS): |
3807 |
"""Executes a remote command after performing strict tests.
|
|
3807 |
"""Executes a restricted command after performing strict tests.
|
|
3808 | 3808 |
|
3809 | 3809 |
@type cmd: string |
3810 | 3810 |
@param cmd: Command name |
... | ... | |
3813 | 3813 |
@raise RPCFail: In case of an error |
3814 | 3814 |
|
3815 | 3815 |
""" |
3816 |
logging.info("Preparing to run remote command '%s'", cmd)
|
|
3816 |
logging.info("Preparing to run restricted command '%s'", cmd)
|
|
3817 | 3817 |
|
3818 | 3818 |
if not _enabled: |
3819 |
_Fail("Remote commands disabled at configure time")
|
|
3819 |
_Fail("Restricted commands disabled at configure time")
|
|
3820 | 3820 |
|
3821 | 3821 |
lock = None |
3822 | 3822 |
try: |
... | ... | |
3844 | 3844 |
# Do not include original error message in returned error |
3845 | 3845 |
_Fail("Executing command '%s' failed" % cmd) |
3846 | 3846 |
elif cmdresult.failed or cmdresult.fail_reason: |
3847 |
_Fail("Remote command '%s' failed: %s; output: %s",
|
|
3847 |
_Fail("Restricted command '%s' failed: %s; output: %s",
|
|
3848 | 3848 |
cmd, cmdresult.fail_reason, cmdresult.output) |
3849 | 3849 |
else: |
3850 | 3850 |
return cmdresult.output |
b/test/py/ganeti.backend_unittest.py | ||
---|---|---|
1 | 1 |
#!/usr/bin/python |
2 | 2 |
# |
3 | 3 |
|
4 |
# Copyright (C) 2010 Google Inc. |
|
4 |
# Copyright (C) 2010, 2013 Google Inc.
|
|
5 | 5 |
# |
6 | 6 |
# This program is free software; you can redistribute it and/or modify |
7 | 7 |
# it under the terms of the GNU General Public License as published by |
... | ... | |
423 | 423 |
_sleep_fn=sleep_fn, _prepare_fn=prepare_fn, |
424 | 424 |
_enabled=True) |
425 | 425 |
except backend.RPCFail, err: |
426 |
self.assertTrue(str(err).startswith("Remote command 'test3079' failed:")) |
|
426 |
self.assertTrue(str(err).startswith("Restricted command 'test3079'" |
|
427 |
" failed:")) |
|
427 | 428 |
self.assertTrue("stderr406328567" in str(err), |
428 | 429 |
msg="Error did not include output") |
429 | 430 |
else: |
... | ... | |
477 | 478 |
_runcmd_fn=NotImplemented, |
478 | 479 |
_enabled=False) |
479 | 480 |
except backend.RPCFail, err: |
480 |
self.assertEqual(str(err), "Remote commands disabled at configure time") |
|
481 |
self.assertEqual(str(err), |
|
482 |
"Restricted commands disabled at configure time") |
|
481 | 483 |
else: |
482 | 484 |
self.fail("Did not raise exception") |
483 | 485 |
|
Also available in: Unified diff