##// END OF EJS Templates
ssh(sec): fix newline problem on key saving that would allow bypassing command sandbox.
marcink -
r2748:2362c2ca stable
parent child Browse files
Show More
@@ -1,125 +1,131 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2
2
3 # Copyright (C) 2016-2018 RhodeCode GmbH
3 # Copyright (C) 2016-2018 RhodeCode GmbH
4 #
4 #
5 # This program is free software: you can redistribute it and/or modify
5 # This program is free software: you can redistribute it and/or modify
6 # it under the terms of the GNU Affero General Public License, version 3
6 # it under the terms of the GNU Affero General Public License, version 3
7 # (only), as published by the Free Software Foundation.
7 # (only), as published by the Free Software Foundation.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU Affero General Public License
14 # You should have received a copy of the GNU Affero General Public License
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 #
16 #
17 # This program is dual-licensed. If you wish to learn more about the
17 # This program is dual-licensed. If you wish to learn more about the
18 # RhodeCode Enterprise Edition, including its added features, Support services,
18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20
20
21 import os
21 import os
22 import stat
22 import stat
23 import logging
23 import logging
24 import tempfile
24 import tempfile
25 import datetime
25 import datetime
26
26
27 from . import config_keys
27 from . import config_keys
28 from rhodecode.model.db import true, joinedload, User, UserSshKeys
28 from rhodecode.model.db import true, joinedload, User, UserSshKeys
29
29
30
30
31 log = logging.getLogger(__name__)
31 log = logging.getLogger(__name__)
32
32
33 HEADER = \
33 HEADER = \
34 "# This file is managed by RhodeCode, please do not edit it manually. # \n" \
34 "# This file is managed by RhodeCode, please do not edit it manually. # \n" \
35 "# Current entries: {}, create date: UTC:{}.\n"
35 "# Current entries: {}, create date: UTC:{}.\n"
36
36
37 # Default SSH options for authorized_keys file, can be override via .ini
37 # Default SSH options for authorized_keys file, can be override via .ini
38 SSH_OPTS = 'no-pty,no-port-forwarding,no-X11-forwarding,no-agent-forwarding'
38 SSH_OPTS = 'no-pty,no-port-forwarding,no-X11-forwarding,no-agent-forwarding'
39
39
40
40
41 def get_all_active_keys():
41 def get_all_active_keys():
42 result = UserSshKeys.query() \
42 result = UserSshKeys.query() \
43 .options(joinedload(UserSshKeys.user)) \
43 .options(joinedload(UserSshKeys.user)) \
44 .filter(UserSshKeys.user != User.get_default_user()) \
44 .filter(UserSshKeys.user != User.get_default_user()) \
45 .filter(User.active == true()) \
45 .filter(User.active == true()) \
46 .all()
46 .all()
47 return result
47 return result
48
48
49
49
50 def _generate_ssh_authorized_keys_file(
50 def _generate_ssh_authorized_keys_file(
51 authorized_keys_file_path, ssh_wrapper_cmd, allow_shell, ssh_opts, debug):
51 authorized_keys_file_path, ssh_wrapper_cmd, allow_shell, ssh_opts, debug):
52
52
53 authorized_keys_file_path = os.path.abspath(
53 authorized_keys_file_path = os.path.abspath(
54 os.path.expanduser(authorized_keys_file_path))
54 os.path.expanduser(authorized_keys_file_path))
55
55
56 import rhodecode
56 import rhodecode
57 all_active_keys = get_all_active_keys()
57 all_active_keys = get_all_active_keys()
58
58
59 if allow_shell:
59 if allow_shell:
60 ssh_wrapper_cmd = ssh_wrapper_cmd + ' --shell'
60 ssh_wrapper_cmd = ssh_wrapper_cmd + ' --shell'
61 if debug:
61 if debug:
62 ssh_wrapper_cmd = ssh_wrapper_cmd + ' --debug'
62 ssh_wrapper_cmd = ssh_wrapper_cmd + ' --debug'
63
63
64 if not os.path.isfile(authorized_keys_file_path):
64 if not os.path.isfile(authorized_keys_file_path):
65 log.debug('Creating file at %s', authorized_keys_file_path)
65 log.debug('Creating file at %s', authorized_keys_file_path)
66 with open(authorized_keys_file_path, 'w'):
66 with open(authorized_keys_file_path, 'w'):
67 pass
67 pass
68
68
69 if not os.access(authorized_keys_file_path, os.R_OK):
69 if not os.access(authorized_keys_file_path, os.R_OK):
70 raise OSError('Access to file {} is without read access'.format(
70 raise OSError('Access to file {} is without read access'.format(
71 authorized_keys_file_path))
71 authorized_keys_file_path))
72
72
73 line_tmpl = '{ssh_opts},command="{wrapper_command} {ini_path} --user-id={user_id} --user={user} --key-id={user_key_id}" {key}\n'
73 line_tmpl = '{ssh_opts},command="{wrapper_command} {ini_path} --user-id={user_id} --user={user} --key-id={user_key_id}" {key}\n'
74
74
75 fd, tmp_authorized_keys = tempfile.mkstemp(
75 fd, tmp_authorized_keys = tempfile.mkstemp(
76 '.authorized_keys_write',
76 '.authorized_keys_write',
77 dir=os.path.dirname(authorized_keys_file_path))
77 dir=os.path.dirname(authorized_keys_file_path))
78
78
79 now = datetime.datetime.utcnow().isoformat()
79 now = datetime.datetime.utcnow().isoformat()
80 keys_file = os.fdopen(fd, 'wb')
80 keys_file = os.fdopen(fd, 'wb')
81 keys_file.write(HEADER.format(len(all_active_keys), now))
81 keys_file.write(HEADER.format(len(all_active_keys), now))
82 ini_path = rhodecode.CONFIG['__file__']
82 ini_path = rhodecode.CONFIG['__file__']
83
83
84 for user_key in all_active_keys:
84 for user_key in all_active_keys:
85 username = user_key.user.username
85 username = user_key.user.username
86 user_id = user_key.user.user_id
86 user_id = user_key.user.user_id
87 # replace all newline from ends and inside
88 safe_key_data = user_key.ssh_key_data\
89 .strip()\
90 .replace('\n', ' ')\
91 .replace('\r', ' ')
87
92
88 keys_file.write(
93 line = line_tmpl.format(
89 line_tmpl.format(
90 ssh_opts=ssh_opts or SSH_OPTS,
94 ssh_opts=ssh_opts or SSH_OPTS,
91 wrapper_command=ssh_wrapper_cmd,
95 wrapper_command=ssh_wrapper_cmd,
92 ini_path=ini_path,
96 ini_path=ini_path,
93 user_id=user_id,
97 user_id=user_id,
94 user=username,
98 user=username,
95 user_key_id=user_key.ssh_key_id,
99 user_key_id=user_key.ssh_key_id,
96 key=user_key.ssh_key_data))
100 key=safe_key_data)
101
102 keys_file.write(line)
97 log.debug('addkey: Key added for user: `%s`', username)
103 log.debug('addkey: Key added for user: `%s`', username)
98 keys_file.close()
104 keys_file.close()
99
105
100 # Explicitly setting read-only permissions to authorized_keys
106 # Explicitly setting read-only permissions to authorized_keys
101 os.chmod(tmp_authorized_keys, stat.S_IRUSR | stat.S_IWUSR)
107 os.chmod(tmp_authorized_keys, stat.S_IRUSR | stat.S_IWUSR)
102 # Rename is atomic operation
108 # Rename is atomic operation
103 os.rename(tmp_authorized_keys, authorized_keys_file_path)
109 os.rename(tmp_authorized_keys, authorized_keys_file_path)
104
110
105
111
106 def generate_ssh_authorized_keys_file(registry):
112 def generate_ssh_authorized_keys_file(registry):
107 log.info('Generating new authorized key file')
113 log.info('Generating new authorized key file')
108
114
109 authorized_keys_file_path = registry.settings.get(
115 authorized_keys_file_path = registry.settings.get(
110 config_keys.authorized_keys_file_path)
116 config_keys.authorized_keys_file_path)
111
117
112 ssh_wrapper_cmd = registry.settings.get(
118 ssh_wrapper_cmd = registry.settings.get(
113 config_keys.wrapper_cmd)
119 config_keys.wrapper_cmd)
114 allow_shell = registry.settings.get(
120 allow_shell = registry.settings.get(
115 config_keys.wrapper_allow_shell)
121 config_keys.wrapper_allow_shell)
116 ssh_opts = registry.settings.get(
122 ssh_opts = registry.settings.get(
117 config_keys.authorized_keys_line_ssh_opts)
123 config_keys.authorized_keys_line_ssh_opts)
118 debug = registry.settings.get(
124 debug = registry.settings.get(
119 config_keys.enable_debug_logging)
125 config_keys.enable_debug_logging)
120
126
121 _generate_ssh_authorized_keys_file(
127 _generate_ssh_authorized_keys_file(
122 authorized_keys_file_path, ssh_wrapper_cmd, allow_shell, ssh_opts,
128 authorized_keys_file_path, ssh_wrapper_cmd, allow_shell, ssh_opts,
123 debug)
129 debug)
124
130
125 return 0
131 return 0
@@ -1,51 +1,51 b''
1 <div class="panel panel-default">
1 <div class="panel panel-default">
2 <div class="panel-heading">
2 <div class="panel-heading">
3 <h3 class="panel-title">${_('New SSH Key generated')}</h3>
3 <h3 class="panel-title">${_('New SSH Key generated')}</h3>
4 </div>
4 </div>
5 <div class="panel-body">
5 <div class="panel-body">
6 <p>
6 <p>
7 ${_('Below is a 2048 bit generated SSH RSA key. You can use it to access RhodeCode via the SSH wrapper.')}
7 ${_('Below is a 2048 bit generated SSH RSA key. You can use it to access RhodeCode via the SSH wrapper.')}
8 </p>
8 </p>
9 <h4>${_('Private key')}</h4>
9 <h4>${_('Private key')}</h4>
10 <pre>
10 <pre>
11 # Save the content as
11 # Save the below content as
12 # Windows: /Users/<username>/.ssh/id_rsa_rhodecode_access_priv.key
12 # Windows: /Users/{username}/.ssh/id_rsa_rhodecode_access_priv.key
13 # macOS: /Users/<yourname>/.ssh/id_rsa_rhodecode_access_priv.key
13 # macOS: /Users/{yourname}/.ssh/id_rsa_rhodecode_access_priv.key
14 # Linux: /home/<username>/.ssh/id_rsa_rhodecode_access_priv.key
14 # Linux: /home/{username}/.ssh/id_rsa_rhodecode_access_priv.key
15
15
16 # Change permissions to 0600 to make it secure, and usable.
16 # Change permissions to 0600 to make it secure, and usable.
17 e.g chmod 0600 /home/<username>/.ssh/id_rsa_rhodecode_access_priv.key
17 e.g chmod 0600 /home/{username}/.ssh/id_rsa_rhodecode_access_priv.key
18 </pre>
18 </pre>
19
19
20 <div>
20 <div>
21 <textarea style="height: 300px">${c.private}</textarea>
21 <textarea style="height: 300px">${c.private}</textarea>
22 </div>
22 </div>
23 <br/>
23 <br/>
24
24
25 <h4>${_('Public key')}</h4>
25 <h4>${_('Public key')}</h4>
26 <pre>
26 <pre>
27 # Save the content as
27 # Save the below content as
28 # Windows: /Users/<username>/.ssh/id_rsa_rhodecode_access_pub.key
28 # Windows: /Users/{username}/.ssh/id_rsa_rhodecode_access_pub.key
29 # macOS: /Users/<yourname>/.ssh/id_rsa_rhodecode_access_pub.key
29 # macOS: /Users/{yourname}/.ssh/id_rsa_rhodecode_access_pub.key
30 # Linux: /home/<username>/.ssh/id_rsa_rhodecode_access_pub.key
30 # Linux: /home/{username}/.ssh/id_rsa_rhodecode_access_pub.key
31 </pre>
31 </pre>
32
32
33 <input type="text" value="${c.public}" class="large text" size="100"/>
33 <input type="text" value="${c.public}" class="large text" size="100"/>
34 <p>
34 <p>
35 % if hasattr(c, 'target_form_url'):
35 % if hasattr(c, 'target_form_url'):
36 <a href="${c.target_form_url}">${_('Use this generated key')}.</a>
36 <a href="${c.target_form_url}">${_('Use this generated key')}.</a>
37 % else:
37 % else:
38 <a href="${h.route_path('edit_user_ssh_keys', user_id=c.user.user_id, _query=dict(default_key=c.public))}">${_('Use this generated key')}.</a>
38 <a href="${h.route_path('edit_user_ssh_keys', user_id=c.user.user_id, _query=dict(default_key=c.public))}">${_('Use this generated key')}.</a>
39 % endif
39 % endif
40 ${_('Confirmation required on the next screen')}.
40 ${_('Confirmation required on the next screen')}.
41 </p>
41 </p>
42 </div>
42 </div>
43 </div>
43 </div>
44
44
45 <script>
45 <script>
46
46
47 $(document).ready(function(){
47 $(document).ready(function(){
48
48
49
49
50 });
50 });
51 </script>
51 </script>
General Comments 0
You need to be logged in to leave comments. Login now