Ensure IPA is locked down in rescue mode

Securely handle state transition by locking down IPA at the final
stage of rescue operation to prevent restarts on tenant networks.

Closes-Bug: #2086865
Change-Id: I8e1be8da93a8c3fdf3cff7ad386c702d970d15f1
This commit is contained in:
cid
2025-02-12 21:42:58 +01:00
parent a6d1921056
commit a42980a016
4 changed files with 43 additions and 2 deletions

View File

@@ -15,6 +15,7 @@
import collections
import importlib.metadata
import ipaddress
import os
import random
import socket
import threading
@@ -564,7 +565,19 @@ class IronicPythonAgent(base.ExecuteCommandMixin):
LOG.error('Neither ipa-api-url nor inspection_callback_url'
'found, please check your pxe append parameters.')
self.serve_ipa_api()
in_rescued_mode = os.path.exists('/etc/.rescued')
if not in_rescued_mode:
self.serve_ipa_api()
else:
# NOTE(cid): In rescued state, we don't call _lockdown_system() as
# it brings down network interfaces which should be preserved for
# rescue operations.
LOG.info('Found rescue state marker file, locking down IPA '
'in disabled mode')
self.heartbeater.stop()
self.serve_api = False
while True:
time.sleep(100)
if not self.standalone and self.api_urls:
self.heartbeater.stop()

View File

@@ -57,6 +57,12 @@ class RescueExtension(base.BaseAgentExtension):
def finalize_rescue(self, rescue_password="", hashed=False):
"""Sets the rescue password for the rescue user."""
self.write_rescue_password(rescue_password, hashed)
try:
open('/etc/.rescued', 'w')
except Exception as exc:
LOG.warning('Failed to create rescue state file marker: %s', exc)
# IPA will terminate after the result of finalize_rescue is returned to
# ironic to avoid exposing the IPA API to a tenant or public network
self.agent.serve_api = False

View File

@@ -84,12 +84,29 @@ class TestRescueExtension(test_base.BaseTestCase):
for passwd in passwds:
self._write_password_hashed_test(passwd)
@mock.patch('builtins.open', autospec=True)
@mock.patch('ironic_python_agent.extensions.rescue.RescueExtension.'
'write_rescue_password', autospec=True)
def test_finalize_rescue(self, mock_write_rescue_password):
def test_finalize_rescue(self, mock_write_rescue_password, mock_open):
self.agent_extension.agent.serve_api = True
self.agent_extension.finalize_rescue(rescue_password='password')
mock_write_rescue_password.assert_called_once_with(
mock.ANY,
rescue_password='password', hashed=False)
self.assertFalse(self.agent_extension.agent.serve_api)
mock_open.assert_called_once_with('/etc/.rescued', 'w')
@mock.patch('builtins.open', autospec=True)
@mock.patch('ironic_python_agent.extensions.rescue.RescueExtension.'
'write_rescue_password', autospec=True)
def test_finalize_rescue_write_failure(self, mock_write_rescue_password,
mock_open):
"""Test that finalize_rescue handles file write failure or no file."""
mock_open.side_effect = IOError("Failed to write file")
self.agent_extension.agent.serve_api = True
self.agent_extension.finalize_rescue(rescue_password='password')
mock_write_rescue_password.assert_called_once_with(
mock.ANY,
rescue_password='password', hashed=False)
self.assertFalse(self.agent_extension.agent.serve_api)
mock_open.assert_called_once_with('/etc/.rescued', 'w')

View File

@@ -0,0 +1,5 @@
---
fixes:
- |
Prevents IPA from restarting on tenant networks during rescue
operations by adding proper lockdown.