diff --git a/devstack/lib/ironic b/devstack/lib/ironic index ca9aeab0e7..c87b65c5ae 100644 --- a/devstack/lib/ironic +++ b/devstack/lib/ironic @@ -479,6 +479,9 @@ IRONIC_DEPLOY_LOGS_LOCAL_PATH=${IRONIC_DEPLOY_LOGS_LOCAL_PATH:-$IRONIC_VM_LOG_DI # Fast track option IRONIC_DEPLOY_FAST_TRACK=${IRONIC_DEPLOY_FAST_TRACK:-False} +# Agent Token requirement +IRONIC_REQUIRE_AGENT_TOKEN=${IRONIC_REQUIRE_AGENT_TOKEN:-True} + # Define baremetal min_microversion in tempest config. Default value None is picked from tempest. TEMPEST_BAREMETAL_MIN_MICROVERSION=${TEMPEST_BAREMETAL_MIN_MICROVERSION:-} @@ -1294,6 +1297,8 @@ function configure_ironic { # Set fast track options iniset $IRONIC_CONF_FILE deploy fast_track $IRONIC_DEPLOY_FAST_TRACK + # Set requirement for agent tokens + iniset $IRONIC_CONF_FILE DEFAULT require_agent_token $IRONIC_REQUIRE_AGENT_TOKEN # No need to check if RabbitMQ is enabled, this call does it in a smart way if [[ "$IRONIC_RPC_TRANSPORT" == "oslo" ]]; then iniset_rpc_backend ironic $IRONIC_CONF_FILE diff --git a/doc/source/admin/agent-token.rst b/doc/source/admin/agent-token.rst new file mode 100644 index 0000000000..90528bc386 --- /dev/null +++ b/doc/source/admin/agent-token.rst @@ -0,0 +1,121 @@ +.. _agent_token: + +=========== +Agent Token +=========== + +Purpose +======= + +The concept of agent tokens is to provide a mechanism by which the +relationship between an operating deployment of the Bare Metal Service +and an instance of the ``ironic-python-agent`` is verified. In a sense, +this token can be viewed as a session identifier or authentication token. + +.. warning:: + This functionality does not remove the risk of a man-in-the-middle attack + that could occur from connection intercept or when TLS is not used for + all communication. + +This becomes useful in the case of deploying an "edge" node where intermediate +networks are not trustworthy. + +How it works +============ + +These tokens are provided in one of two ways to the running agent. + +1. A pre-generated token which is embedded into virtual media ISOs. +2. A one-time generated token that are provided upon the first "lookup" + of the node. + +In both cases, the tokens are a randomly generated length of 128 characters. + +Once the token has been provided, the token cannot be retrieved or accessed. +It remains available to the conductors, and is stored in memory of the +``ironic-python-agent``. + +.. note:: + In the case of the token being embedded with virtual media, it is read + from a configuration file with-in the image. Ideally this should be paired + with Swift temporary URLs. + +With the token is available in memory in the agent, the token is embedded with +``heartbeat`` operations to the ironic API endpoint. This enables the API to +authenticate the heartbeat request, and refuse "heartbeat" requests from the +``ironic-python-agent``. With the ``Ussuri`` release, the confiuration option +``[DEFAULT]require_agent_token`` can be set ``True`` to explicitly require +token use. + +.. warning:: + If the Bare Metal Service is updated, and the version of + ``ironic-python-agent`` should be updated to enable this feature. + +In addition to heartbeats being verified, commands from the +``ironic-conductor`` service to the ``ironic-python-agent`` also include the +token, allowing the agent to authenticate the caller. + + +With Virtual Media +------------------ + +.. seqdiag:: + :scale: 80 + + diagram { + API; Conductor; Baremetal; Swift; IPA; + activation = none; + span_height = 1; + edge_length = 250; + default_note_color = white; + default_fontsize = 14; + + Conductor -> Conductor [label = "Generates a random token"]; + Conductor -> Conductor [label = "Generates configuration for IPA ramdisk"]; + Conductor -> Swift [label = "IPA image, with configuration is uploaded"]; + Conductor -> Baremetal [label = "Attach IPA virtual media in Swift as virtual CD"]; + Conductor -> Baremetal [label = "Conductor turns power on"]; + Baremetal -> Swift [label = "Baremetal reads virtual media"]; + Baremetal -> Baremetal [label = "Boots IPA virtual media image"]; + Baremetal -> Baremetal [label = "IPA is started"]; + IPA -> Baremetal [label = "IPA loads configuration and agent token into memory"]; + IPA -> API [label = "Lookup node"]; + API -> IPA [label = "API responds with node UUID and token value of '******'"]; + IPA -> API [label = "Heartbeat with agent token"]; + } + +With PXE/iPXE/etc. +------------------ + +.. seqdiag:: + :scale: 80 + + diagram { + API; Conductor; Baremetal; iPXE; IPA; + activation = none; + span_height = 1; + edge_length = 250; + default_note_color = white; + default_fontsize = 14; + + Conductor -> Baremetal [label = "Conductor turns power on"]; + Baremetal -> iPXE [label = "Baremetal reads kernel/ramdisk and starts boot"]; + Baremetal -> Baremetal [label = "Boots IPA virtual media image"]; + Baremetal -> Baremetal [label = "IPA is started"]; + IPA -> Baremetal [label = "IPA loads configuration"]; + IPA -> API [label = "Lookup node"]; + API -> Conductor [label = "API requests conductor to generates a random token"]; + API -> IPA [label = "API responds with node UUID and token value"]; + IPA -> API [label = "Heartbeat with agent token"]; + } + +Agent Configuration +=================== + +An additional setting which may be leveraged with the ``ironic-python-agent`` +is a ``agent_token_required`` setting. Under normal circumstances, this +setting can be asserted via the configuration supplied from the Bare Metal +service deployment upon the ``lookup`` action, but can be asserted via the +embedded configuration for the agent in the ramdisk. This setting is also +available via kernel command line as ``ipa-agent-token-required``. + diff --git a/doc/source/admin/index.rst b/doc/source/admin/index.rst index 9f2a0dd666..2ce43e33cf 100644 --- a/doc/source/admin/index.rst +++ b/doc/source/admin/index.rst @@ -32,6 +32,7 @@ the services. Windows Images Troubleshooting FAQ Power Sync with the Compute Service + Agent Token .. toctree:: :hidden: diff --git a/ironic/api/controllers/v1/ramdisk.py b/ironic/api/controllers/v1/ramdisk.py index 625b98ccc3..0f22501b4f 100644 --- a/ironic/api/controllers/v1/ramdisk.py +++ b/ironic/api/controllers/v1/ramdisk.py @@ -53,7 +53,10 @@ def config(token): 'statsd_port': CONF.metrics_statsd.agent_statsd_port }, 'heartbeat_timeout': CONF.api.ramdisk_heartbeat_timeout, - 'agent_token': token + 'agent_token': token, + # Not an API version based indicator, passing as configuration + # as the signifigants indicates support should also be present. + 'agent_token_required': CONF.require_agent_token, } diff --git a/ironic/tests/unit/api/controllers/v1/test_ramdisk.py b/ironic/tests/unit/api/controllers/v1/test_ramdisk.py index 7ff1084432..2321466978 100644 --- a/ironic/tests/unit/api/controllers/v1/test_ramdisk.py +++ b/ironic/tests/unit/api/controllers/v1/test_ramdisk.py @@ -62,7 +62,7 @@ class TestLookup(test_api_base.BaseApiTest): self.mock_get_node_with_token.return_value = node def _check_config(self, data): - expected_metrics = { + expected_config = { 'metrics': { 'backend': 'statsd', 'prepend_host': CONF.metrics.agent_prepend_host, @@ -76,9 +76,10 @@ class TestLookup(test_api_base.BaseApiTest): 'statsd_port': CONF.metrics_statsd.agent_statsd_port }, 'heartbeat_timeout': CONF.api.ramdisk_heartbeat_timeout, - 'agent_token': mock.ANY + 'agent_token': mock.ANY, + 'agent_token_required': False, } - self.assertEqual(expected_metrics, data['config']) + self.assertEqual(expected_config, data['config']) self.assertIsNotNone(data['config']['agent_token']) self.assertNotEqual('******', data['config']['agent_token']) diff --git a/playbooks/legacy/grenade-dsvm-ironic-multinode-multitenant/run.yaml b/playbooks/legacy/grenade-dsvm-ironic-multinode-multitenant/run.yaml index e3fb7806cb..faea30335d 100644 --- a/playbooks/legacy/grenade-dsvm-ironic-multinode-multitenant/run.yaml +++ b/playbooks/legacy/grenade-dsvm-ironic-multinode-multitenant/run.yaml @@ -151,6 +151,8 @@ export DEVSTACK_LOCAL_CONFIG+=$'\n'"IRONIC_VM_COUNT=7" + export DEVSTACK_LOCAL_CONFIG+=$'\n'"IRONIC_REQUIRE_AGENT_TOKEN=False" + # Ensure the ironic-vars-EARLY file exists touch ironic-vars-early # Pull in the EARLY variables injected by the optional builders diff --git a/playbooks/legacy/grenade-dsvm-ironic/run.yaml b/playbooks/legacy/grenade-dsvm-ironic/run.yaml index 5609efed35..bf38ff9cd2 100644 --- a/playbooks/legacy/grenade-dsvm-ironic/run.yaml +++ b/playbooks/legacy/grenade-dsvm-ironic/run.yaml @@ -97,6 +97,8 @@ export DEVSTACK_LOCAL_CONFIG+=$'\n'"IRONIC_VM_COUNT=7" + export DEVSTACK_LOCAL_CONFIG+=$'\n'"IRONIC_REQUIRE_AGENT_TOKEN=False" + # Ensure the ironic-vars-EARLY file exists touch ironic-vars-early # Pull in the EARLY variables injected by the optional builders diff --git a/releasenotes/notes/agent-token-support-0a5b5aa1585dfbb5.yaml b/releasenotes/notes/agent-token-support-0a5b5aa1585dfbb5.yaml new file mode 100644 index 0000000000..f133e0365b --- /dev/null +++ b/releasenotes/notes/agent-token-support-0a5b5aa1585dfbb5.yaml @@ -0,0 +1,19 @@ +--- +features: + - | + Adds support of ``agent token`` which serves as a mechanism to secure + the normally unauthenticated API endpoints in ironic which are used in + the mechanics of baremetal provisioning. This feature is optional, however + operators may require this feature by changing the + ``[DEFAULT]require_agent_token`` setting to ``True``. +upgrades: + - | + In order to use the new Agent Token support, all ramdisk settings should + be updated for all nodes in ironic. If token use is required by ironic's + configuration, and the ramdisks have not been updated, then all + deployment, cleaning, and rescue operations will fail until the version of + the ironic-python-agent ramdisk has been updated. +issues: + - | + The ``ansible`` deployment interface does not support use of an + ``agent token`` at this time.