Add support for AWS IMDSv2
This is an authenticated http metadata service which is typically available by default, but a more secure setup is to enforce its usage. This change adds the ability to do that for both instances and AMIs. Change-Id: Ia8554ff0baec260289da0574b92932b37ffe5f04
This commit is contained in:
parent
42f9100d82
commit
3f4fb008b0
@ -431,6 +431,15 @@ Selecting the ``aws`` driver adds the following options to the
|
||||
:value:`providers.[aws].diskimages.import-method.snapshot`
|
||||
import method.
|
||||
|
||||
.. attr:: imds-support
|
||||
:type: str
|
||||
|
||||
To enforce usage of IMDSv2 by default on instances created
|
||||
from the image, set this value to `v2.0`. If omitted, IMDSv2
|
||||
is optional by default. This is only supported using the
|
||||
:value:`providers.[aws].diskimages.import-method.snapshot`
|
||||
import method.
|
||||
|
||||
.. attr:: import-method
|
||||
:default: snapshot
|
||||
|
||||
@ -669,6 +678,29 @@ Selecting the ``aws`` driver adds the following options to the
|
||||
ARN identifier of the profile.
|
||||
Mutually exclusive with :attr:`providers.[aws].pools.labels.iam-instance-profile.name`
|
||||
|
||||
.. attr:: imdsv2
|
||||
:type: str
|
||||
|
||||
Specify whether IMDSv2 is required. If this is omitted,
|
||||
then AWS defaults are used (usually equivalent to
|
||||
`optional` but may be influenced by the image used).
|
||||
|
||||
.. value:: optional
|
||||
|
||||
Allows usage of IMDSv2 but do not require it. This
|
||||
sets the following metadata options:
|
||||
|
||||
* `HttpTokens` is `optional`
|
||||
* `HttpEndpoint` is `enabled`
|
||||
|
||||
.. value:: required
|
||||
|
||||
Require IMDSv2. This sets the following metadata
|
||||
options:
|
||||
|
||||
* `HttpTokens` is `required`
|
||||
* `HttpEndpoint` is `enabled`
|
||||
|
||||
.. attr:: key-name
|
||||
:type: string
|
||||
:required:
|
||||
|
@ -512,6 +512,12 @@ class AwsAdapter(statemachine.Adapter):
|
||||
bucket = self.s3.Bucket(bucket_name)
|
||||
object_filename = f'{image_name}.{image_format}'
|
||||
extra_args = {'Tagging': urllib.parse.urlencode(metadata)}
|
||||
|
||||
# There is no IMDS support option for the import_image call
|
||||
if (provider_image.import_method == 'image' and
|
||||
provider_image.imds_support == 'v2.0'):
|
||||
raise Exception("IMDSv2 requires 'snapshot' import method")
|
||||
|
||||
with open(filename, "rb") as fobj:
|
||||
with self.rate_limiter:
|
||||
bucket.upload_fileobj(fobj, object_filename,
|
||||
@ -618,7 +624,7 @@ class AwsAdapter(statemachine.Adapter):
|
||||
if provider_image.throughput:
|
||||
bdm['Ebs']['Throughput'] = provider_image.throughput
|
||||
|
||||
register_response = self.ec2_client.register_image(
|
||||
args = dict(
|
||||
Architecture=provider_image.architecture,
|
||||
BlockDeviceMappings=[bdm],
|
||||
RootDeviceName='/dev/sda1',
|
||||
@ -626,6 +632,9 @@ class AwsAdapter(statemachine.Adapter):
|
||||
EnaSupport=provider_image.ena_support,
|
||||
Name=image_name,
|
||||
)
|
||||
if provider_image.imds_support == 'v2.0':
|
||||
args['ImdsSupport'] = 'v2.0'
|
||||
register_response = self.ec2_client.register_image(**args)
|
||||
|
||||
# Tag the AMI
|
||||
try:
|
||||
@ -1211,6 +1220,17 @@ class AwsAdapter(statemachine.Adapter):
|
||||
}
|
||||
}
|
||||
|
||||
if label.imdsv2 == 'required':
|
||||
args['MetadataOptions'] = {
|
||||
'HttpTokens': 'required',
|
||||
'HttpEndpoint': 'enabled',
|
||||
}
|
||||
elif label.imdsv2 == 'optional':
|
||||
args['MetadataOptions'] = {
|
||||
'HttpTokens': 'optional',
|
||||
'HttpEndpoint': 'enabled',
|
||||
}
|
||||
|
||||
with self.rate_limiter(log.debug, "Created instance"):
|
||||
log.debug("Creating VM %s", hostname)
|
||||
resp = self.ec2_client.run_instances(**args)
|
||||
|
@ -105,6 +105,10 @@ class AwsProviderDiskImage(ConfigValue):
|
||||
self.volume_size = image.get('volume-size', None)
|
||||
self.volume_type = image.get('volume-type', 'gp2')
|
||||
self.import_method = image.get('import-method', 'snapshot')
|
||||
self.imds_support = image.get('imds-support', None)
|
||||
if (self.imds_support == 'v2.0' and
|
||||
self.import_method != 'snapshot'):
|
||||
raise Exception("IMDSv2 requires 'snapshot' import method")
|
||||
self.iops = image.get('iops', None)
|
||||
self.throughput = image.get('throughput', None)
|
||||
|
||||
@ -128,6 +132,7 @@ class AwsProviderDiskImage(ConfigValue):
|
||||
'volume-size': int,
|
||||
'volume-type': str,
|
||||
'import-method': v.Any('snapshot', 'image'),
|
||||
'imds-support': v.Any('v2.0', None),
|
||||
'iops': int,
|
||||
'throughput': int,
|
||||
'tags': dict,
|
||||
@ -180,6 +185,7 @@ class AwsLabel(ConfigValue):
|
||||
self.dynamic_tags = label.get('dynamic-tags', {})
|
||||
self.host_key_checking = self.pool.host_key_checking
|
||||
self.use_spot = bool(label.get('use-spot', False))
|
||||
self.imdsv2 = label.get('imdsv2', None)
|
||||
|
||||
@staticmethod
|
||||
def getSchema():
|
||||
@ -202,6 +208,7 @@ class AwsLabel(ConfigValue):
|
||||
'tags': dict,
|
||||
'dynamic-tags': dict,
|
||||
'use-spot': bool,
|
||||
'imdsv2': v.Any(None, 'required', 'optional'),
|
||||
}
|
||||
|
||||
|
||||
|
6
nodepool/tests/fixtures/aws/aws.yaml
vendored
6
nodepool/tests/fixtures/aws/aws.yaml
vendored
@ -24,6 +24,7 @@ labels:
|
||||
- name: ubuntu1404-with-tags
|
||||
- name: ubuntu1404-with-shell-type
|
||||
- name: ubuntu1404-ebs-optimized
|
||||
- name: ubuntu1404-imdsv2
|
||||
|
||||
providers:
|
||||
- name: ec2-us-west-2
|
||||
@ -115,3 +116,8 @@ providers:
|
||||
ebs-optimized: True
|
||||
instance-type: t3.medium
|
||||
key-name: zuul
|
||||
- name: ubuntu1404-imdsv2
|
||||
cloud-image: ubuntu1404
|
||||
instance-type: t3.medium
|
||||
key-name: zuul
|
||||
imdsv2: required
|
||||
|
68
nodepool/tests/fixtures/aws/diskimage-imdsv2-image.yaml
vendored
Normal file
68
nodepool/tests/fixtures/aws/diskimage-imdsv2-image.yaml
vendored
Normal file
@ -0,0 +1,68 @@
|
||||
elements-dir: .
|
||||
images-dir: '{images_dir}'
|
||||
build-log-dir: '{build_log_dir}'
|
||||
build-log-retention: 1
|
||||
|
||||
zookeeper-servers:
|
||||
- host: {zookeeper_host}
|
||||
port: {zookeeper_port}
|
||||
chroot: {zookeeper_chroot}
|
||||
|
||||
zookeeper-tls:
|
||||
ca: {zookeeper_ca}
|
||||
cert: {zookeeper_cert}
|
||||
key: {zookeeper_key}
|
||||
|
||||
tenant-resource-limits:
|
||||
- tenant-name: tenant-1
|
||||
max-cores: 1024
|
||||
|
||||
labels:
|
||||
- name: diskimage
|
||||
|
||||
providers:
|
||||
- name: ec2-us-west-2
|
||||
driver: aws
|
||||
rate: 2
|
||||
region-name: us-west-2
|
||||
object-storage:
|
||||
bucket-name: nodepool
|
||||
image-import-timeout: 60
|
||||
diskimages:
|
||||
- name: fake-image
|
||||
tags:
|
||||
provider_metadata: provider
|
||||
import-method: image
|
||||
iops: 1000
|
||||
throughput: 100
|
||||
imds-support: v2.0
|
||||
pools:
|
||||
- name: main
|
||||
max-servers: 1
|
||||
subnet-id: {subnet_id}
|
||||
security-group-id: {security_group_id}
|
||||
node-attributes:
|
||||
key1: value1
|
||||
key2: value2
|
||||
labels:
|
||||
- name: diskimage
|
||||
diskimage: fake-image
|
||||
instance-type: t3.medium
|
||||
key-name: zuul
|
||||
iops: 2000
|
||||
throughput: 200
|
||||
|
||||
diskimages:
|
||||
- name: fake-image
|
||||
elements:
|
||||
- fedora-minimal
|
||||
- vm
|
||||
release: 21
|
||||
dib-cmd: nodepool/tests/fake-image-create
|
||||
env-vars:
|
||||
TMPDIR: /opt/dib_tmp
|
||||
DIB_IMAGE_CACHE: /opt/dib_cache
|
||||
DIB_CLOUD_IMAGES: http://download.fedoraproject.org/pub/fedora/linux/releases/test/21-Beta/Cloud/Images/x86_64/
|
||||
BASE_IMAGE_FILE: Fedora-Cloud-Base-20141029-21_Beta.x86_64.qcow2
|
||||
metadata:
|
||||
diskimage_metadata: diskimage
|
69
nodepool/tests/fixtures/aws/diskimage-imdsv2-snapshot.yaml
vendored
Normal file
69
nodepool/tests/fixtures/aws/diskimage-imdsv2-snapshot.yaml
vendored
Normal file
@ -0,0 +1,69 @@
|
||||
elements-dir: .
|
||||
images-dir: '{images_dir}'
|
||||
build-log-dir: '{build_log_dir}'
|
||||
build-log-retention: 1
|
||||
|
||||
zookeeper-servers:
|
||||
- host: {zookeeper_host}
|
||||
port: {zookeeper_port}
|
||||
chroot: {zookeeper_chroot}
|
||||
|
||||
zookeeper-tls:
|
||||
ca: {zookeeper_ca}
|
||||
cert: {zookeeper_cert}
|
||||
key: {zookeeper_key}
|
||||
|
||||
tenant-resource-limits:
|
||||
- tenant-name: tenant-1
|
||||
max-cores: 1024
|
||||
|
||||
labels:
|
||||
- name: diskimage
|
||||
|
||||
providers:
|
||||
- name: ec2-us-west-2
|
||||
driver: aws
|
||||
rate: 2
|
||||
region-name: us-west-2
|
||||
object-storage:
|
||||
bucket-name: nodepool
|
||||
image-import-timeout: 60
|
||||
diskimages:
|
||||
- name: fake-image
|
||||
tags:
|
||||
provider_metadata: provider
|
||||
volume-type: gp3
|
||||
iops: 1000
|
||||
throughput: 100
|
||||
imds-support: v2.0
|
||||
pools:
|
||||
- name: main
|
||||
max-servers: 1
|
||||
subnet-id: {subnet_id}
|
||||
security-group-id: {security_group_id}
|
||||
node-attributes:
|
||||
key1: value1
|
||||
key2: value2
|
||||
labels:
|
||||
- name: diskimage
|
||||
diskimage: fake-image
|
||||
instance-type: t3.medium
|
||||
key-name: zuul
|
||||
iops: 2000
|
||||
throughput: 200
|
||||
|
||||
diskimages:
|
||||
- name: fake-image
|
||||
elements:
|
||||
- fedora-minimal
|
||||
- vm
|
||||
release: 21
|
||||
dib-cmd: nodepool/tests/fake-image-create
|
||||
env-vars:
|
||||
TMPDIR: /opt/dib_tmp
|
||||
DIB_IMAGE_CACHE: /opt/dib_cache
|
||||
DIB_CLOUD_IMAGES: http://download.fedoraproject.org/pub/fedora/linux/releases/test/21-Beta/Cloud/Images/x86_64/
|
||||
BASE_IMAGE_FILE: Fedora-Cloud-Base-20141029-21_Beta.x86_64.qcow2
|
||||
metadata:
|
||||
diskimage_metadata: diskimage
|
||||
username: another_user
|
@ -52,6 +52,11 @@ class FakeAwsAdapter(AwsAdapter):
|
||||
self.__testcase.run_instance_calls.append(kwargs)
|
||||
return self.ec2_client.run_instances_orig(*args, **kwargs)
|
||||
|
||||
# The ImdsSupport parameter isn't handled by moto
|
||||
def _fake_register_image(*args, **kwargs):
|
||||
self.__testcase.register_image_calls.append(kwargs)
|
||||
return self.ec2_client.register_image_orig(*args, **kwargs)
|
||||
|
||||
def _fake_get_paginator(*args, **kwargs):
|
||||
try:
|
||||
return self.__testcase.fake_aws.get_paginator(*args, **kwargs)
|
||||
@ -60,6 +65,8 @@ class FakeAwsAdapter(AwsAdapter):
|
||||
|
||||
self.ec2_client.run_instances_orig = self.ec2_client.run_instances
|
||||
self.ec2_client.run_instances = _fake_run_instances
|
||||
self.ec2_client.register_image_orig = self.ec2_client.register_image
|
||||
self.ec2_client.register_image = _fake_register_image
|
||||
self.ec2_client.import_snapshot = \
|
||||
self.__testcase.fake_aws.import_snapshot
|
||||
self.ec2_client.import_image = \
|
||||
@ -157,8 +164,9 @@ class TestDriverAws(tests.DBTestCase):
|
||||
Bucket='nodepool',
|
||||
CreateBucketConfiguration={'LocationConstraint': 'us-west-2'})
|
||||
|
||||
# A list of args to create instance for validation
|
||||
# A list of args to method calls for validation
|
||||
self.run_instance_calls = []
|
||||
self.register_image_calls = []
|
||||
|
||||
# TEST-NET-3
|
||||
ipv6 = False
|
||||
@ -568,6 +576,9 @@ class TestDriverAws(tests.DBTestCase):
|
||||
response = instance.describe_attribute(Attribute='ebsOptimized')
|
||||
self.assertFalse(response['EbsOptimized']['Value'])
|
||||
|
||||
self.assertFalse(
|
||||
'MetadataOptions' in self.run_instance_calls[0])
|
||||
|
||||
node.state = zk.USED
|
||||
self.zk.storeNode(node)
|
||||
self.waitForNodeDeletion(node)
|
||||
@ -755,6 +766,20 @@ class TestDriverAws(tests.DBTestCase):
|
||||
response = instance.describe_attribute(Attribute='ebsOptimized')
|
||||
self.assertTrue(response['EbsOptimized']['Value'])
|
||||
|
||||
def test_aws_imdsv2(self):
|
||||
req = self.requestNode('aws/aws.yaml',
|
||||
'ubuntu1404-imdsv2')
|
||||
node = self.assertSuccess(req)
|
||||
self.assertEqual(node.host_keys, ['ssh-rsa FAKEKEY'])
|
||||
self.assertEqual(node.image_id, 'ubuntu1404')
|
||||
|
||||
self.assertEqual(
|
||||
self.run_instance_calls[0]['MetadataOptions']['HttpTokens'],
|
||||
'required')
|
||||
self.assertEqual(
|
||||
self.run_instance_calls[0]['MetadataOptions']['HttpEndpoint'],
|
||||
'enabled')
|
||||
|
||||
def test_aws_invalid_instance_type(self):
|
||||
req = self.requestNode('aws/aws-invalid.yaml', 'ubuntu-invalid')
|
||||
self.assertEqual(req.state, zk.FAILED)
|
||||
@ -782,6 +807,7 @@ class TestDriverAws(tests.DBTestCase):
|
||||
|
||||
ec2_image = self.ec2.Image(image.external_id)
|
||||
self.assertEqual(ec2_image.state, 'available')
|
||||
self.assertFalse('ImdsSupport' in self.register_image_calls[0])
|
||||
self.assertTrue({'Key': 'diskimage_metadata', 'Value': 'diskimage'}
|
||||
in ec2_image.tags)
|
||||
self.assertTrue({'Key': 'provider_metadata', 'Value': 'provider'}
|
||||
@ -858,6 +884,60 @@ class TestDriverAws(tests.DBTestCase):
|
||||
self.run_instance_calls[0]['BlockDeviceMappings'][0]['Ebs']
|
||||
['Throughput'], 200)
|
||||
|
||||
def test_aws_diskimage_snapshot_imdsv2(self):
|
||||
self.fake_aws.fail_import_count = 1
|
||||
configfile = self.setup_config('aws/diskimage-imdsv2-snapshot.yaml')
|
||||
|
||||
self.useBuilder(configfile)
|
||||
|
||||
image = self.waitForImage('ec2-us-west-2', 'fake-image')
|
||||
self.assertEqual(image.username, 'another_user')
|
||||
|
||||
ec2_image = self.ec2.Image(image.external_id)
|
||||
self.assertEqual(ec2_image.state, 'available')
|
||||
self.assertEqual(
|
||||
self.register_image_calls[0]['ImdsSupport'], 'v2.0')
|
||||
|
||||
self.assertTrue({'Key': 'diskimage_metadata', 'Value': 'diskimage'}
|
||||
in ec2_image.tags)
|
||||
self.assertTrue({'Key': 'provider_metadata', 'Value': 'provider'}
|
||||
in ec2_image.tags)
|
||||
|
||||
pool = self.useNodepool(configfile, watermark_sleep=1)
|
||||
self.startPool(pool)
|
||||
|
||||
req = zk.NodeRequest()
|
||||
req.state = zk.REQUESTED
|
||||
req.node_types.append('diskimage')
|
||||
|
||||
self.zk.storeNodeRequest(req)
|
||||
req = self.waitForNodeRequest(req)
|
||||
|
||||
self.assertEqual(req.state, zk.FULFILLED)
|
||||
self.assertNotEqual(req.nodes, [])
|
||||
node = self.zk.getNode(req.nodes[0])
|
||||
self.assertEqual(node.allocated_to, req.id)
|
||||
self.assertEqual(node.state, zk.READY)
|
||||
self.assertIsNotNone(node.launcher)
|
||||
self.assertEqual(node.connection_type, 'ssh')
|
||||
self.assertEqual(node.shell_type, None)
|
||||
self.assertEqual(node.username, 'another_user')
|
||||
self.assertEqual(node.attributes,
|
||||
{'key1': 'value1', 'key2': 'value2'})
|
||||
self.assertEqual(
|
||||
self.run_instance_calls[0]['BlockDeviceMappings'][0]['Ebs']
|
||||
['Iops'], 2000)
|
||||
self.assertEqual(
|
||||
self.run_instance_calls[0]['BlockDeviceMappings'][0]['Ebs']
|
||||
['Throughput'], 200)
|
||||
|
||||
def test_aws_diskimage_image_imdsv2(self):
|
||||
self.fake_aws.fail_import_count = 1
|
||||
configfile = self.setup_config('aws/diskimage-imdsv2-image.yaml')
|
||||
|
||||
with testtools.ExpectedException(Exception, "IMDSv2 requires"):
|
||||
self.useBuilder(configfile)
|
||||
|
||||
def test_aws_diskimage_removal(self):
|
||||
configfile = self.setup_config('aws/diskimage.yaml')
|
||||
self.useBuilder(configfile)
|
||||
|
6
releasenotes/notes/imdsv2-44e9e973b6c2a562.yaml
Normal file
6
releasenotes/notes/imdsv2-44e9e973b6c2a562.yaml
Normal file
@ -0,0 +1,6 @@
|
||||
---
|
||||
features:
|
||||
- |
|
||||
Support for requiring IMDSv2 in AWS is now available using these options:
|
||||
:attr:`providers.[aws].pools.labels.imdsv2` and
|
||||
:attr:`providers.[aws].diskimages.imds-support`
|
Loading…
x
Reference in New Issue
Block a user