Skip to content

Commit fdd11b5

Browse files
committed
Configure and use SSL-related requests options
This patch adds standard SSL options to IPA config and makes use of them when making HTTP requests. For now, a single set of certificates is used when needed. In the future configuration can be expanded to allow per-service certificates. Besides, the 'insecure' option (defaults to False) can be overridden through kernel command line parameter 'ipa-insecure'. This will allow running IPA in CI-like environments with self-signed SSL certificates. Change-Id: I259d9b3caa9ba1dc3d7382f375b8e086a5348d80 Closes-Bug: #1642515
1 parent 51ab461 commit fdd11b5

File tree

12 files changed

+198
-5
lines changed

12 files changed

+198
-5
lines changed

doc/source/index.rst

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -226,6 +226,74 @@ ironic-python-agent.service unit in cloud-config.yaml [5]_.
226226
* ``--debug``: Enables debug logging.
227227

228228

229+
IPA and SSL
230+
===========
231+
232+
During its operation IPA makes HTTP requests to a number of other services,
233+
currently including
234+
235+
- ironic for lookup/heartbeats
236+
- ironic-inspector to publish results of introspection
237+
- HTTP image storage to fetch the user image to be written to the node's disk
238+
(Object storage service or other service storing user images
239+
when ironic is running in a standalone mode)
240+
241+
When these services are configured to require SSL-encrypted connections,
242+
IPA can be configured to either properly use such secure connections or
243+
ignore verifying such SSL connections.
244+
245+
Configuration mostly happens in the IPA config file
246+
(default is ``/etc/ironic_python_agent/ironic_python_agent.conf``)
247+
or command line arguments passed to ``ironic-python-agent``,
248+
and it is possible to provide some options via kernel command line arguments
249+
instead.
250+
251+
Available options in the ``[DEFAULT]`` config file section are:
252+
253+
insecure
254+
Whether to verify server SSL certificates.
255+
When not specified explicitly, defaults to the value of ``ipa-insecure``
256+
kernel command line argument (converted to boolean).
257+
The default for this kernel command line argument is taken to be ``False``.
258+
Overriding it to ``True`` by adding ``ipa-insecure=1`` to the value of
259+
``[pxe]pxe_append_params`` in ironic configuration file will allow running
260+
the same IPA-based deploy ramdisk in a CI-like environment when services
261+
are using secure HTTPS endpoints with self-signed certificates without
262+
adding a custom CA file to the deploy ramdisk (see below).
263+
264+
cafile
265+
Path to the PEM encoded Certificate Authority file.
266+
When not specified, available system-wide list of CAs will be used to
267+
verify server certificates.
268+
Thus in order to use IPA with HTTPS endpoints of other services in
269+
a secure fashion (with ``insecure`` option being ``False``, see above),
270+
operators should either ensure that certificates of those services
271+
are verifiable by root CAs present in the deploy ramdisk,
272+
or add a custom CA file to the ramdisk and set this IPA option to point
273+
to this file at ramdisk build time.
274+
275+
certfile
276+
Path to PEM encoded client certificate cert file.
277+
This option must be used when services are configured to require client
278+
certificates on SSL-secured connections.
279+
This cert file must be added to the deploy ramdisk and path
280+
to it specified for IPA via this option at ramdisk build time.
281+
This option has an effect only when the ``keyfile`` option is also set.
282+
283+
keyfile
284+
Path to PEM encoded client certificate key file.
285+
This option must be used when services are configured to require client
286+
certificates on SSL-secured connections.
287+
This key file must be added to the deploy ramdisk and path
288+
to it specified for IPA via this option at ramdisk build time.
289+
This option has an effect only when the ``certfile`` option is also set.
290+
291+
Currently a single set of cafile/certfile/keyfile options is used for all
292+
HTTP requests to the other services.
293+
294+
Securing IPA's HTTP server itself with SSL is not yet supported in default
295+
ramdisk builds.
296+
229297
Hardware Managers
230298
=================
231299

etc/ironic_python_agent/ironic_python_agent.conf.sample

Lines changed: 25 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,9 +5,10 @@
55
#
66

77
# URL of the Ironic API. Can be supplied as "ipa-api-url"
8-
# kernel parameter. (string value)
8+
# kernel parameter.The value must start with either http:// or
9+
# https://. (string value)
910
# Deprecated group/name - [DEFAULT]/api_url
10-
#api_url = http://127.0.0.1:6385
11+
#api_url = <None>
1112

1213
# The IP address to listen on. Can be supplied as "ipa-listen-
1314
# host" kernel parameter. (string value)
@@ -43,7 +44,7 @@
4344
# Deprecated group/name - [DEFAULT]/ip_lookup_sleep
4445
#ip_lookup_sleep = 10
4546

46-
# The interface to use when looking for an IPaddress. Can be
47+
# The interface to use when looking for an IP address. Can be
4748
# supplied as "ipa-network-interface" kernel parameter.
4849
# (string value)
4950
# Deprecated group/name - [DEFAULT]/network_interface
@@ -122,6 +123,27 @@
122123
# (integer value)
123124
#disk_wait_delay = 3
124125

126+
# Verify HTTPS connections. Can be supplied as "ipa-insecure"
127+
# kernel parameter. (boolean value)
128+
#insecure = false
129+
130+
# Path to PEM encoded Certificate Authority file to use when
131+
# verifying HTTPS connections. Default is to use available
132+
# system-wide configured CAs. (string value)
133+
#cafile = <None>
134+
135+
# Path to PEM encoded client certificate cert file. Must be
136+
# provided together with "keyfile" option. Default is to not
137+
# present any client certificates to the server. (string
138+
# value)
139+
#certfile = <None>
140+
141+
# Path to PEM encoded client certificate key file. Must be
142+
# provided together with "certfile" option. Default is to not
143+
# present any client certificates to the server. (string
144+
# value)
145+
#keyfile = <None>
146+
125147
#
126148
# From oslo.log
127149
#

ironic_python_agent/agent.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -162,6 +162,9 @@ def __init__(self, api_url, advertise_address, listen_address,
162162
lookup_timeout, lookup_interval, standalone,
163163
hardware_initialization_delay=0):
164164
super(IronicPythonAgent, self).__init__()
165+
if bool(cfg.CONF.keyfile) != bool(cfg.CONF.certfile):
166+
LOG.warning("Only one of 'keyfile' and 'certfile' options is "
167+
"defined in config file. Its value will be ignored.")
165168
self.ext_mgr = extension.ExtensionManager(
166169
namespace='ironic_python_agent.extensions',
167170
invoke_on_load=True,

ironic_python_agent/config.py

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -180,6 +180,24 @@
180180
'in inventory. '
181181
'Can be supplied as "ipa-disk-wait-delay" '
182182
'kernel parameter.'),
183+
cfg.BoolOpt('insecure',
184+
default=APARAMS.get('ipa-insecure', False),
185+
help='Verify HTTPS connections. Can be supplied as '
186+
'"ipa-insecure" kernel parameter.'),
187+
cfg.StrOpt('cafile',
188+
help='Path to PEM encoded Certificate Authority file '
189+
'to use when verifying HTTPS connections. '
190+
'Default is to use available system-wide configured CAs.'),
191+
cfg.StrOpt('certfile',
192+
help='Path to PEM encoded client certificate cert file. '
193+
'Must be provided together with "keyfile" option. '
194+
'Default is to not present any client certificates to '
195+
'the server.'),
196+
cfg.StrOpt('keyfile',
197+
help='Path to PEM encoded client certificate key file. '
198+
'Must be provided together with "certfile" option. '
199+
'Default is to not present any client certificates to '
200+
'the server.'),
183201
]
184202

185203
CONF.register_cli_opts(cli_opts)

ironic_python_agent/extensions/standby.py

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
import time
2020

2121
from oslo_concurrency import processutils
22+
from oslo_config import cfg
2223
from oslo_log import log
2324

2425
from ironic_lib import disk_utils
@@ -27,6 +28,7 @@
2728
from ironic_python_agent import hardware
2829
from ironic_python_agent import utils
2930

31+
CONF = cfg.CONF
3032
LOG = log.getLogger(__name__)
3133

3234
IMAGE_CHUNK_SIZE = 1024 * 1024 # 1MB
@@ -227,7 +229,9 @@ def _download_file(self, image_info, url):
227229
if no_proxy:
228230
os.environ['no_proxy'] = no_proxy
229231
proxies = image_info.get('proxies', {})
230-
resp = requests.get(url, stream=True, proxies=proxies)
232+
verify, cert = utils.get_ssl_client_options(CONF)
233+
resp = requests.get(url, stream=True, proxies=proxies,
234+
verify=verify, cert=cert)
231235
if resp.status_code != 200:
232236
msg = ('Received status code {} from {}, expected 200. Response '
233237
'body: {}').format(resp.status_code, url, resp.text)

ironic_python_agent/inspector.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -118,7 +118,9 @@ def call_inspector(data, failures):
118118
encoder = encoding.RESTJSONEncoder()
119119
data = encoder.encode(data)
120120

121-
resp = requests.post(CONF.inspection_callback_url, data=data)
121+
verify, cert = utils.get_ssl_client_options(CONF)
122+
resp = requests.post(CONF.inspection_callback_url, data=data,
123+
verify=verify, cert=cert)
122124
if resp.status_code >= 400:
123125
LOG.error('inspector error %d: %s, proceeding with lookup',
124126
resp.status_code, resp.content.decode('utf-8'))

ironic_python_agent/ironic_api_client.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
# limitations under the License.
1414

1515

16+
from oslo_config import cfg
1617
from oslo_log import log
1718
from oslo_serialization import jsonutils
1819
from oslo_service import loopingcall
@@ -21,8 +22,10 @@
2122
from ironic_python_agent import encoding
2223
from ironic_python_agent import errors
2324
from ironic_python_agent import netutils
25+
from ironic_python_agent import utils
2426

2527

28+
CONF = cfg.CONF
2629
LOG = log.getLogger(__name__)
2730

2831

@@ -57,10 +60,13 @@ def _request(self, method, path, data=None, headers=None, **kwargs):
5760
'Accept': 'application/json',
5861
})
5962

63+
verify, cert = utils.get_ssl_client_options(CONF)
6064
return self.session.request(method,
6165
request_url,
6266
headers=headers,
6367
data=data,
68+
verify=verify,
69+
cert=cert,
6470
**kwargs)
6571

6672
def heartbeat(self, uuid, advertise_address):

ironic_python_agent/tests/unit/extensions/test_standby.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -299,6 +299,7 @@ def test_download_image(self, requests_mock, open_mock, md5_mock):
299299

300300
standby._download_image(image_info)
301301
requests_mock.assert_called_once_with(image_info['urls'][0],
302+
cert=None, verify=True,
302303
stream=True, proxies={})
303304
write = file_mock.write
304305
write.assert_any_call('some')
@@ -329,6 +330,7 @@ def test_download_image_proxy(
329330
standby._download_image(image_info)
330331
self.assertEqual(no_proxy, os.environ['no_proxy'])
331332
requests_mock.assert_called_once_with(image_info['urls'][0],
333+
cert=None, verify=True,
332334
stream=True, proxies=proxies)
333335
write = file_mock.write
334336
write.assert_any_call('some')
@@ -767,6 +769,7 @@ def test_stream_raw_image_onto_device(self, requests_mock, open_mock,
767769
self.agent_extension._stream_raw_image_onto_device(image_info,
768770
'/dev/foo')
769771
requests_mock.assert_called_once_with(image_info['urls'][0],
772+
cert=None, verify=True,
770773
stream=True, proxies={})
771774
expected_calls = [mock.call('some'), mock.call('content')]
772775
file_mock.write.assert_has_calls(expected_calls)
@@ -790,6 +793,7 @@ def test_stream_raw_image_onto_device_write_error(self, requests_mock,
790793
self.agent_extension._stream_raw_image_onto_device,
791794
image_info, '/dev/foo')
792795
requests_mock.assert_called_once_with(image_info['urls'][0],
796+
cert=None, verify=True,
793797
stream=True, proxies={})
794798
# Assert write was only called once and failed!
795799
file_mock.write.assert_called_once_with('some')
@@ -863,5 +867,6 @@ def test_download_image(self, requests_mock, md5_mock):
863867

864868
self.assertEqual(content, list(image_download))
865869
requests_mock.assert_called_once_with(image_info['urls'][0],
870+
cert=None, verify=True,
866871
stream=True, proxies={})
867872
self.assertEqual(image_info['checksum'], image_download.md5sum())

ironic_python_agent/tests/unit/test_inspector.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -145,6 +145,7 @@ def test_ok(self, mock_post):
145145
res = inspector.call_inspector(data, failures)
146146

147147
mock_post.assert_called_once_with('url',
148+
cert=None, verify=True,
148149
data='{"data": 42, "error": null}')
149150
self.assertEqual(mock_post.return_value.json.return_value, res)
150151

@@ -157,6 +158,7 @@ def test_send_failure(self, mock_post):
157158
res = inspector.call_inspector(data, failures)
158159

159160
mock_post.assert_called_once_with('url',
161+
cert=None, verify=True,
160162
data='{"data": 42, "error": "boom"}')
161163
self.assertEqual(mock_post.return_value.json.return_value, res)
162164

@@ -168,6 +170,7 @@ def test_inspector_error(self, mock_post):
168170
res = inspector.call_inspector(data, failures)
169171

170172
mock_post.assert_called_once_with('url',
173+
cert=None, verify=True,
171174
data='{"data": 42, "error": null}')
172175
self.assertIsNone(res)
173176

ironic_python_agent/tests/unit/test_utils.py

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -455,3 +455,33 @@ def test_collect_system_logs_non_journald(
455455
file_list=['/var/log'],
456456
io_dict={'iptables': mock.ANY, 'ip_addr': mock.ANY, 'ps': mock.ANY,
457457
'dmesg': mock.ANY, 'df': mock.ANY})
458+
459+
def test_get_ssl_client_options(self):
460+
# defaults
461+
conf = mock.Mock(insecure=False, cafile=None,
462+
keyfile=None, certfile=None)
463+
self.assertEqual((True, None), utils.get_ssl_client_options(conf))
464+
465+
# insecure=True overrides cafile
466+
conf = mock.Mock(insecure=True, cafile='spam',
467+
keyfile=None, certfile=None)
468+
self.assertEqual((False, None), utils.get_ssl_client_options(conf))
469+
470+
# cafile returned as verify when not insecure
471+
conf = mock.Mock(insecure=False, cafile='spam',
472+
keyfile=None, certfile=None)
473+
self.assertEqual(('spam', None), utils.get_ssl_client_options(conf))
474+
475+
# only both certfile and keyfile produce non-None result
476+
conf = mock.Mock(insecure=False, cafile=None,
477+
keyfile=None, certfile='ham')
478+
self.assertEqual((True, None), utils.get_ssl_client_options(conf))
479+
480+
conf = mock.Mock(insecure=False, cafile=None,
481+
keyfile='ham', certfile=None)
482+
self.assertEqual((True, None), utils.get_ssl_client_options(conf))
483+
484+
conf = mock.Mock(insecure=False, cafile=None,
485+
keyfile='spam', certfile='ham')
486+
self.assertEqual((True, ('ham', 'spam')),
487+
utils.get_ssl_client_options(conf))

0 commit comments

Comments
 (0)