CVE-2019-3895
Description
CVE-2019-3895 is an access-control flaw in Octavia allowing remote attackers to run amphorae with arbitrary images.
AI Insight
LLM-synthesized narrative grounded in this CVE's description and references.
CVE-2019-3895 is an access-control flaw in Octavia allowing remote attackers to run amphorae with arbitrary images.
Vulnerability
CVE-2019-3895 is an access-control flaw in the Octavia load-balancer service when deployed via Red Hat OpenStack Platform Director [1]. The issue allows an attacker to upload a custom image and cause new amphorae to be spawned using that arbitrary image [2].
Exploitation
An attacker with network access to the Octavia API could upload a malicious image to Glance and then request new amphorae. Octavia would use the compromised image without proper ownership validation [4].
Impact
An attacker could run arbitrary code within the amphora containers, potentially compromising the load-balancing infrastructure and gaining access to tenant traffic or other cloud resources [3].
Mitigation
Red Hat released updates for RHOSP 13 and 14 (RHSA-2019:1683 and RHSA-2019:1742) to fix the issue [2][3]. The fix involves setting the amp_image_owner_id to restrict image usage to the service project [4].
AI Insight generated on May 22, 2026. Synthesized from this CVE's description and the cited reference URLs; citations are validated against the source bundle.
Affected packages
Versions sourced from the GitHub Security Advisory.
| Package | Affected versions | Patched versions |
|---|---|---|
octaviaPyPI | < 0.9.0 | 0.9.0 |
Affected products
2- Range: n/a
Patches
2e7c5eab712e0[CVE-2019-3895] Set image owner id
4 files changed · +62 −6
playbooks/octavia-files.yaml+1 −0 modified@@ -69,6 +69,7 @@ ca_private_key_path: "{{ ca_private_key_path }}" ca_passphrase: "{{ ca_passphrase }}" client_cert_path: "{{ client_cert_path }}" + auth_project_name: "{{ auth_project_name }}" environment: OS_USERNAME: "{{ os_username }}" OS_USER_DOMAIN_NAME: "Default"
playbooks/roles/octavia-controller-config/tasks/octavia.yml+12 −0 modified@@ -41,3 +41,15 @@ src: "manager-post-deploy.conf.j2" selevel: s0 setype: svirt_sandbox_file_t + - name: gather facts about the service project + shell: | + openstack project show "{{ auth_project_name }}" -c id -f value + register: project_id_result + - name: setting [controller_worker]/amp_image_owner_id + become: true + become_user: root + ini_file: + path: "{{ octavia_confd_prefix }}/etc/octavia/conf.d/common/post-deploy.conf" + section: controller_worker + option: amp_image_owner_id + value: "{{ project_id_result.stdout }}"
playbooks/roles/octavia-undercloud/tasks/image_mgmt.yml+39 −6 modified@@ -19,13 +19,38 @@ amphora_image: "{{ (image_file_result.stat.path | basename | splitext)[0] }}" when: amphora_image is not defined and image_file_result.stat.exists and not symlnk_check.stat.islnk - - name: check there an image in glance already + - name: gather facts about the service project shell: | - openstack image show {{ amphora_image }} -c checksum -f value + openstack project show "{{ auth_project_name }}" -c id -f value + register: project_id_result + + - name: check there's an image in glance already + shell: | + openstack image list --property owner={{ project_id_result.stdout }} --private --name {{ amphora_image }} -c ID -f value + environment: + OS_USERNAME: "{{ auth_username }}" + OS_PASSWORD: "{{ auth_password }}" + OS_PROJECT_NAME: "{{ auth_project_name }}" + register: glance_id_result + ignore_errors: true + + - name: set image id fact + set_fact: + image_id: "{{ glance_id_result.stdout }}" + when: glance_id_result.rc == 0 + + - name: get checksum if there's an image in glance already + shell: | + openstack image show {{ glance_id_result.stdout }} -c checksum -f value + environment: + OS_USERNAME: "{{ auth_username }}" + OS_PASSWORD: "{{ auth_password }}" + OS_PROJECT_NAME: "{{ auth_project_name }}" + when: image_id is defined register: glance_results ignore_errors: true - - name: get md5 from glance if image already exists there + - name: set current_md5 fact from glance if image already exists there set_fact: current_md5: "{{ glance_results.stdout }}" when: glance_results.rc == 0 @@ -37,10 +62,14 @@ - name: move existing image if the names match and the md5s are not the same shell: | - ts=`openstack image show {{ amphora_image }} -f value -c created_at` + ts=`openstack image show {{ image_id }} -f value -c created_at` ts=${ts//:/} ts=${ts//-/} - openstack image set {{ amphora_image }} --name "{{ amphora_image }}_$ts" + openstack image set {{ image_id }} --name "{{ amphora_image }}_$ts" + environment: + OS_USERNAME: "{{ auth_username }}" + OS_PASSWORD: "{{ auth_password }}" + OS_PROJECT_NAME: "{{ auth_project_name }}" when: replace_image is defined and replace_image - name: decide whether to upload new image @@ -73,7 +102,11 @@ --container-format bare --tag {{ amp_image_tag }} \ --file {{ raw_filename|default(image_filename) }} \ --property hw_architecture={{ amp_hw_arch }} \ - {{ amphora_image }} + --private {{ amphora_image }} + environment: + OS_USERNAME: "{{ auth_username }}" + OS_PASSWORD: "{{ auth_password }}" + OS_PROJECT_NAME: "{{ auth_project_name }}" register: image_result changed_when: "image_result.stdout != ''" when: image_file_result.stat.exists and upload_image is defined
releasenotes/notes/octavia-set-image-owner-id-adb197d5daae54f1.yaml+10 −0 added@@ -0,0 +1,10 @@ +--- +security: + - | + Fixed a vulnerability where an attacker may cause new Octavia amphorae to + run based on any arbitrary image (CVE-2019-3895). +fixes: + - | + Ensure [controller_worker]/amp_image_owner_id is set. This configuration + option restricts Glance image selection to a specific owner ID. This is a + recommended security setting.
d7d062a47ab5Option to restrict amp glance image owner
11 files changed · +71 −27
devstack/plugin.sh+5 −0 modified@@ -25,6 +25,11 @@ function build_octavia_worker_image { $OCTAVIA_DIR/diskimage-create/diskimage-create.sh -s 2 -o $OCTAVIA_AMP_IMAGE_FILE fi upload_image file://${OCTAVIA_AMP_IMAGE_FILE} $TOKEN + + image_id=$(glance image-list --property-filter name=${OCTAVIA_AMP_IMAGE_NAME} | awk '/ amphora-x64-haproxy / {print $2}') + owner_id=$(glance image-show ${image_id} | awk '/ owner / {print $4}') + iniset $OCTAVIA_CONF controller_worker amp_image_owner_id ${owner_id} + } function create_octavia_accounts {
etc/octavia.conf+3 −0 modified@@ -135,6 +135,9 @@ # parameters is needed. Using tags is the recommended way to refer to images. # amp_image_id = # amp_image_tag = +# Optional owner ID used to restrict glance images to one owner ID. +# This is a recommended security setting. +# amp_image_owner_id = # Nova parameters to use when booting amphora # amp_flavor_id = # amp_ssh_key_name =
octavia/common/config.py+4 −0 modified@@ -216,6 +216,10 @@ deprecated_for_removal=True, deprecated_reason='Superseded by amp_image_tag option.', help=_('Glance image id for the Amphora image to boot')), + cfg.StrOpt('amp_image_owner_id', + default='', + help=_('Restrict glance image selection to a specific ' + 'owner ID. This is a recommended security setting.')), cfg.StrOpt('amp_ssh_key_name', default='', help=_('SSH key name used to boot the Amphora')),
octavia/common/constants.py+1 −0 modified@@ -331,3 +331,4 @@ AMP_ACTION_START = 'start' AMP_ACTION_STOP = 'stop' AMP_ACTION_RELOAD = 'reload' +GLANCE_IMAGE_ACTIVE = 'active'
octavia/compute/compute_base.py+1 −1 modified@@ -22,7 +22,7 @@ class ComputeBase(object): @abc.abstractmethod def build(self, name="amphora_name", amphora_flavor=None, - image_id=None, image_tag=None, + image_id=None, image_tag=None, image_owner=None, key_name=None, sec_groups=None, network_ids=None, config_drive_files=None, user_data=None, server_group_id=None): """Build a new amphora.
octavia/compute/drivers/noop_driver/driver.py+9 −9 modified@@ -28,22 +28,22 @@ def __init__(self): self.computeconfig = {} def build(self, name="amphora_name", amphora_flavor=None, - image_id=None, image_tag=None, + image_id=None, image_tag=None, image_owner=None, key_name=None, sec_groups=None, network_ids=None, config_drive_files=None, user_data=None, port_ids=None, server_group_id=None): LOG.debug("Compute %s no-op, build name %s, amphora_flavor %s, " - "image_id %s, image_tag %s, key_name %s, sec_groups %s, " - "network_ids %s, config_drive_files %s, user_data %s, " - "port_ids %s, server_group_id %s", + "image_id %s, image_tag %s, image_owner %s, key_name %s, " + "sec_groups %s, network_ids %s, config_drive_files %s, " + "user_data %s, port_ids %s, server_group_id %s", self.__class__.__name__, - name, amphora_flavor, image_id, image_tag, + name, amphora_flavor, image_id, image_tag, image_owner, key_name, sec_groups, network_ids, config_drive_files, user_data, port_ids, server_group_id) self.computeconfig[(name, amphora_flavor, image_id, image_tag, - key_name, user_data)] = ( + image_owner, key_name, user_data)] = ( name, amphora_flavor, - image_id, image_tag, key_name, sec_groups, + image_id, image_tag, image_owner, key_name, sec_groups, network_ids, config_drive_files, user_data, port_ids, 'build') compute_id = uuidutils.generate_uuid() @@ -87,13 +87,13 @@ def __init__(self): self.driver = NoopManager() def build(self, name="amphora_name", amphora_flavor=None, - image_id=None, image_tag=None, + image_id=None, image_tag=None, image_owner=None, key_name=None, sec_groups=None, network_ids=None, config_drive_files=None, user_data=None, port_ids=None, server_group_id=None): compute_id = self.driver.build(name, amphora_flavor, - image_id, image_tag, + image_id, image_tag, image_owner, key_name, sec_groups, network_ids, config_drive_files, user_data, port_ids, server_group_id)
octavia/compute/drivers/nova_driver.py+20 −10 modified@@ -32,11 +32,21 @@ CONF.import_group('nova', 'octavia.common.config') -def _extract_amp_image_id_by_tag(client, image_tag): - images = list(client.images.list( - filters={'tag': [image_tag]}, - sort='created_at:desc', - limit=2)) +def _extract_amp_image_id_by_tag(client, image_tag, image_owner): + if image_owner: + images = list(client.images.list( + filters={'tag': [image_tag], + 'owner': image_owner, + 'status': constants.GLANCE_IMAGE_ACTIVE}, + sort='created_at:desc', + limit=2)) + else: + images = list(client.images.list( + filters={'tag': [image_tag], + 'status': constants.GLANCE_IMAGE_ACTIVE}, + sort='created_at:desc', + limit=2)) + if not images: raise exceptions.GlanceNoTaggedImages(tag=image_tag) image_id = images[0]['id'] @@ -50,15 +60,15 @@ def _extract_amp_image_id_by_tag(client, image_tag): return image_id -def _get_image_uuid(client, image_id, image_tag): +def _get_image_uuid(client, image_id, image_tag, image_owner): if image_id: if image_tag: LOG.warning( _LW("Both amp_image_id and amp_image_tag options defined. " - "Using the former.")) + "Using the amp_image_id.")) return image_id - return _extract_amp_image_id_by_tag(client, image_tag) + return _extract_amp_image_id_by_tag(client, image_tag, image_owner) class VirtualMachineManager(compute_base.ComputeBase): @@ -84,7 +94,7 @@ def __init__(self): self.server_groups = self._nova_client.server_groups def build(self, name="amphora_name", amphora_flavor=None, - image_id=None, image_tag=None, + image_id=None, image_tag=None, image_owner=None, key_name=None, sec_groups=None, network_ids=None, port_ids=None, config_drive_files=None, user_data=None, server_group_id=None): @@ -126,7 +136,7 @@ def build(self, name="amphora_name", amphora_flavor=None, "group": server_group_id} image_id = _get_image_uuid( - self._glance_client, image_id, image_tag) + self._glance_client, image_id, image_tag, image_owner) amphora = self.manager.create( name=name, image=image_id, flavor=amphora_flavor, key_name=key_name, security_groups=sec_groups,
octavia/controller/worker/tasks/compute_tasks.py+1 −0 modified@@ -83,6 +83,7 @@ def execute(self, amphora_id, ports=None, config_drive_files=None, amphora_flavor=CONF.controller_worker.amp_flavor_id, image_id=CONF.controller_worker.amp_image_id, image_tag=CONF.controller_worker.amp_image_tag, + image_owner=CONF.controller_worker.amp_image_owner_id, key_name=key_name, sec_groups=CONF.controller_worker.amp_secgroup_list, network_ids=network_ids,
octavia/tests/unit/compute/drivers/test_nova_driver.py+9 −7 modified@@ -35,18 +35,19 @@ def test__get_image_uuid_tag(self): with mock.patch.object(nova_common, '_extract_amp_image_id_by_tag', return_value='fakeid') as extract: - image_id = nova_common._get_image_uuid(client, '', 'faketag') + image_id = nova_common._get_image_uuid(client, '', 'faketag', None) self.assertEqual('fakeid', image_id) - extract.assert_called_with(client, 'faketag') + extract.assert_called_with(client, 'faketag', None) def test__get_image_uuid_notag(self): client = mock.Mock() - image_id = nova_common._get_image_uuid(client, 'fakeid', '') + image_id = nova_common._get_image_uuid(client, 'fakeid', '', None) self.assertEqual('fakeid', image_id) def test__get_image_uuid_id_beats_tag(self): client = mock.Mock() - image_id = nova_common._get_image_uuid(client, 'fakeid', 'faketag') + image_id = nova_common._get_image_uuid(client, 'fakeid', + 'faketag', None) self.assertEqual('fakeid', image_id) @@ -62,15 +63,16 @@ def test_no_images(self): self.client.images.list.return_value = [] self.assertRaises( exceptions.GlanceNoTaggedImages, - nova_common._extract_amp_image_id_by_tag, self.client, 'faketag') + nova_common._extract_amp_image_id_by_tag, self.client, + 'faketag', None) def test_single_image(self): images = [ {'id': uuidutils.generate_uuid(), 'tag': 'faketag'} ] self.client.images.list.return_value = images image_id = nova_common._extract_amp_image_id_by_tag(self.client, - 'faketag') + 'faketag', None) self.assertIn(image_id, images[0]['id']) def test_multiple_images_returns_one_of_images(self): @@ -80,7 +82,7 @@ def test_multiple_images_returns_one_of_images(self): ] self.client.images.list.return_value = images image_id = nova_common._extract_amp_image_id_by_tag(self.client, - 'faketag') + 'faketag', None) self.assertIn(image_id, [image['id'] for image in images])
octavia/tests/unit/controller/worker/tasks/test_compute_tasks.py+12 −0 modified@@ -87,6 +87,11 @@ def setUp(self): @mock.patch('stevedore.driver.DriverManager.driver') def test_compute_create(self, mock_driver, mock_conf, mock_jinja): + image_owner_id = uuidutils.generate_uuid() + conf = oslo_fixture.Config(cfg.CONF) + conf.config(group="controller_worker", + amp_image_owner_id=image_owner_id) + createcompute = compute_tasks.ComputeCreate() mock_driver.build.return_value = COMPUTE_ID @@ -100,6 +105,7 @@ def test_compute_create(self, mock_driver, mock_conf, mock_jinja): amphora_flavor=AMP_FLAVOR_ID, image_id=AMP_IMAGE_ID, image_tag=AMP_IMAGE_TAG, + image_owner=image_owner_id, key_name=AMP_SSH_KEY_NAME, sec_groups=AMP_SEC_GROUPS, network_ids=AMP_NET, @@ -134,6 +140,9 @@ def test_compute_create(self, mock_driver, mock_conf, mock_jinja): createcompute.revert(COMPUTE_ID, _amphora_mock.id) + conf.config(group="controller_worker", + amp_image_owner_id='') + @mock.patch('jinja2.Environment.get_template') @mock.patch('octavia.amphorae.backends.agent.' 'agent_jinja_cfg.AgentJinjaTemplater.' @@ -160,6 +169,7 @@ def test_compute_create_user_data(self, mock_driver, amphora_flavor=AMP_FLAVOR_ID, image_id=AMP_IMAGE_ID, image_tag=AMP_IMAGE_TAG, + image_owner='', key_name=AMP_SSH_KEY_NAME, sec_groups=AMP_SEC_GROUPS, network_ids=AMP_NET, @@ -219,6 +229,7 @@ def test_compute_create_without_ssh_access(self, mock_driver, amphora_flavor=AMP_FLAVOR_ID, image_id=AMP_IMAGE_ID, image_tag=AMP_IMAGE_TAG, + image_owner='', key_name=None, sec_groups=AMP_SEC_GROUPS, network_ids=AMP_NET, @@ -276,6 +287,7 @@ def test_compute_create_cert(self, mock_driver, mock_conf, mock_jinja): amphora_flavor=AMP_FLAVOR_ID, image_id=AMP_IMAGE_ID, image_tag=AMP_IMAGE_TAG, + image_owner='', key_name=AMP_SSH_KEY_NAME, sec_groups=AMP_SEC_GROUPS, network_ids=AMP_NET,
releasenotes/notes/glance_image_owner-42c92a12f91a62a6.yaml+6 −0 added@@ -0,0 +1,6 @@ +--- +security: + - Allows the operator to optionally restrict the amphora + glance image selection to a specific owner id. + This is a recommended security setting for clouds that + allow user uploadable images.
Vulnerability mechanics
Generated on May 9, 2026. Inputs: CWE entries + fix-commit diffs from this CVE's patches. Citations validated against bundle.
References
12- access.redhat.com/errata/RHSA-2019:1683ghsavendor-advisoryx_refsource_REDHATWEB
- access.redhat.com/errata/RHSA-2019:1742ghsavendor-advisoryx_refsource_REDHATWEB
- github.com/advisories/GHSA-jjgh-m322-fjx6ghsaADVISORY
- nvd.nist.gov/vuln/detail/CVE-2019-3895ghsaADVISORY
- bugs.launchpad.net/octavia/+bug/1620629ghsaWEB
- bugs.launchpad.net/tripleo/+bug/1830607ghsaWEB
- bugzilla.redhat.com/show_bug.cgighsax_refsource_CONFIRMWEB
- github.com/openstack/octavia/blob/08570831754d9671fbd1756d668f55f191e47ca4/octavia/compute/drivers/nova_driver.pyghsaWEB
- github.com/openstack/octavia/commit/d7d062a47ab54a540d81f13a0e5f3085ebfaa0d2ghsaWEB
- github.com/openstack/tripleo-common/commit/e7c5eab712e0f70ecbc6d225d4766e0fe0f3f884ghsaWEB
- github.com/pypa/advisory-database/tree/main/vulns/octavia/PYSEC-2019-194.yamlghsaWEB
- opendev.org/openstack/octavia/commit/d7d062a47ab54a540d81f13a0e5f3085ebfaa0d2ghsaWEB
News mentions
0No linked articles in our index yet.