Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
36 changes: 36 additions & 0 deletions examples/aws-custom-master-key.nix
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
{
resources.awsCustomerMasterKey.cmk =
{lib, ...}:
{
alias = "nixops-kms";
description = "nixops is the best";
policy = builtins.toJSON
{
Statement= [
{
Effect= "Allow";
Principal = "*";
Action = "*";
Resource= "*";
}
];
};
origin = "AWS_KMS";
deletionWaitPeriod = 7;
region = "us-east-1";
accessKeyId = "testing";
tags = { name = "nixops-managed-cmk";};
};
resources.ebsVolumes.ebs =
{resources, ...}:
{
region = "us-east-1";
accessKeyId = "testing";
size = 50;
volumeType = "gp2";
kmsKeyId = resources.awsCustomerMasterKey.cmk;
zone = "us-east-1a";
tags = { name = "nixops"; env = "test";};
};

}
71 changes: 71 additions & 0 deletions nix/aws-customer-master-key.nix
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
{ config, lib, uuid, name, ... }:

with lib;

{
imports = [ ./common-ec2-auth-options.nix ];

options = {

alias = mkOption {
default = "nixops-${uuid}-${name}";
type = types.str;
description = "Alias of the CMK.";
};

keyId = mkOption {
default = "";
type = types.str;
description = "The globally unique identifier for the CMK. This is set by NixOps";
};

policy = mkOption {
default = null;
type = types.nullOr types.str;
description = ''
The key policy to attach to the CMK.
'';
};

description = mkOption {
default = "CMK created by nixops";
type = types.str;
description = "A description of the CMK.";
};

origin = mkOption {
default = "AWS_KMS";
type = types.enum [ "AWS_KMS" "AWS_CLOUDHSM" ];
description = ''
The source of the key material for the CMK.
You cannot change the origin after you create the CMK.
There is also an EXTERNAL option but it is not supported
in nixops yet.
'';
};

customKeyStoreId = mkOption {
default = null;
type = types.nullOr types.str;
description = ''
Creates the CMK in the specified custom key store and the key
material in its associated AWS CloudHSM cluster. To create a CMK
in a custom key store, you must also specify the Origin parameter
with a value of "AWS_CLOUDHSM" .
'';
};

deletionWaitPeriod = mkOption {
default = 30;
type = types.int;
description = ''
The waiting period, specified in number of days. After
the waiting period ends, AWS KMS deletes the customer master key (CMK).
Valid values are between 7 and 30.
'';
};

} // import ./common-ec2-options.nix { inherit lib; };

config._type = "aws-customer-master-key";
}
1 change: 1 addition & 0 deletions nix/default.nix
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@
awsVPNGateways = evalResources ./aws-vpn-gateway.nix (zipAttrs resourcesByType.awsVPNGateways or []);
awsVPNConnections = evalResources ./aws-vpn-connection.nix (zipAttrs resourcesByType.awsVPNConnections or []);
awsVPNConnectionRoutes = evalResources ./aws-vpn-connection-route.nix (zipAttrs resourcesByType.awsVPNConnectionRoutes or []);
awsCustomerMasterKey = evalResources ./aws-customer-master-key.nix (zipAttrs resourcesByType.awsCustomerMasterKey or []);
};
}

12 changes: 12 additions & 0 deletions nix/ebs-volume.nix
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
{ config, lib, uuid, name, ... }:

with lib;
with import ./lib.nix lib;

{

Expand Down Expand Up @@ -49,6 +50,17 @@ with lib;
'';
};

kmsKeyId = mkOption {
default = null;
type = with types; nullOr (either types.str (resource "aws-customer-master-key"));
apply = x: if builtins.isString x then x else "res-" + x._name;
description = ''
The identifier of the AWS Key Management Service (AWS KMS)
customer master key (CMK) to use for Amazon EBS encryption.
If this parameter is not specified, your AWS managed CMK for EBS is used.
'';
};

} // import ./common-ec2-options.nix { inherit lib; };

config = {
Expand Down
1 change: 1 addition & 0 deletions nixopsaws/resources/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -36,3 +36,4 @@
import vpc_route_table
import vpc_route_table_association
import vpc_subnet
import aws_customer_master_key
187 changes: 187 additions & 0 deletions nixopsaws/resources/aws_customer_master_key.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,187 @@
# -*- coding: utf-8 -*-

# Automatic provisioning of AWS CMK.
import os
import boto3
import botocore

import nixops.util
import nixops.resources
from nixopsaws.resources.ec2_common import EC2CommonState
import nixopsaws.ec2_utils
from nixops.state import StateDict
from nixops.diff import Diff, Handler

class awsCustomerMasterKeyDefinition(nixops.resources.ResourceDefinition):
"""Definition of an aws customer master key."""

@classmethod
def get_type(cls):
return "aws-customer-master-key"

@classmethod
def get_resource_type(cls):
return "awsCustomerMasterKey"

def show_type(self):
return "{0}".format(self.get_type())


class awsCustomerMasterKeyState(nixops.resources.DiffEngineResourceState, EC2CommonState):
"""State of a CMK."""

state = nixops.util.attr_property("state", nixops.resources.ResourceState.MISSING, int)
access_key_id = nixops.util.attr_property("accessKeyId", None)
_reserved_keys = EC2CommonState.COMMON_EC2_RESERVED + ["keyId"]

@classmethod
def get_type(cls):
return "aws-customer-master-key"

def __init__(self, depl, name, id):
nixops.resources.DiffEngineResourceState.__init__(self, depl, name, id)
self.keyId = self._state.get('keyId', None)
self.handle_create = Handler(['deletionWaitPeriod', 'origin', 'region', 'customKeyStoreId'],
handle=self.realize_create_cmk)
self.handle_description = Handler(['description'], after=[self.handle_create],
handle=self.realize_update_description)
self.handle_policy = Handler(['policy'], after=[self.handle_create], handle=self.realize_policy)
self.handle_alias = Handler(['alias'], after=[self.handle_create], handle=self.realize_update_alias)
self.handle_tag_update = Handler(['tags'], after=[self.handle_create], handle=self.realize_update_tag)

def show_type(self):
s = super(awsCustomerMasterKeyState, self).show_type()
region = self._state.get('region', None)
if region: s = "{0} [{1}]".format(s, region)
return s

@property
def resource_id(self):
return self._state.get('keyId', None)

def prefix_definition(self, attr):
return {('resources', 'awsCustomerMasterKey'): attr}

def get_physical_spec(self):
return { 'cmkId': self._state.get('keyId', None)}

def get_definition_prefix(self):
return "resources.awsCustomerMasterKey."

def realize_create_cmk(self, allow_recreate):
"""Handle both create and recreate of the aws customer master key resource """
config = self.get_defn()
if self.state == self.UP:
if not allow_recreate:
raise Exception("aws customer master key {} definition changed and it needs to be recreated "
"use --allow-recreate if you want to create a new one".format(self.keyId))
self.warn("aws customer master key definition changed, recreating...")
self._destroy()
self._client = None

self._state["region"] = config['region']

self.log("creating aws customer master key under region {0}".format(config['region']))
args = dict(
KeyUsage='ENCRYPT_DECRYPT',
Origin = config['origin'],
)
if config['customKeyStoreId']:
args['CustomKeyStoreId'] = config['customKeyStoreId']
cmk = self.get_client(service="kms").create_key(**args)
self.keyId = cmk['KeyMetadata']['KeyId']

with self.depl._db:
self.state = self.UP
self._state["keyId"] = self.keyId
self._state["region"] = config['region']
self._state["origin"] = config['origin']
self._state["deletionWaitPeriod"] = config['deletionWaitPeriod']

def realize_update_description(self, allow_recreate):
config = self.get_defn()
self.get_client(service="kms").update_key_description(KeyId=self.keyId, Description=config['description'])

with self.depl._db:
self._state['description'] = config['description']

def realize_policy(self, allow_recreate):
config = self.get_defn()
self.log("updating `{0}` policy...".format(self.keyId))
self.get_client(service="kms").put_key_policy(KeyId=self.keyId, PolicyName="default", Policy=config['policy'])

with self.depl._db:
self._state['policy'] = config['policy']

def realize_update_tag(self, allow_recreate):
config = self.get_defn()
tags = config['tags']
tags.update(self.get_common_tags())
self.get_client(service="kms").tag_resource(KeyId=self.keyId, Tags=[{"TagKey": k, "TagValue": tags[k]} for k in tags])

def realize_update_alias(self, allow_recreate):
config = self.get_defn()
# we don't want to have many alias for a key so we delete the old one before creating a new
if self._state.get('subnetId', None):
self.get_client(service="kms").delete_alias(AliasName="alias/" + self._state['alias'])
self.log("updating `{0}` alias...".format(self.keyId))
else:
self.log("creating alias for `{0}`...".format(self.keyId))
self.get_client(service="kms").create_alias(TargetKeyId=self.keyId, AliasName="alias/" + config['alias'])

with self.depl._db:
self._state['alias'] = config['alias']

def _check(self):
if self._state.get('keyId', None) is None:
return
try:
cmk = self.get_client(service="kms").describe_key(KeyId=self._state["keyId"])
except botocore.exceptions.ClientError as e:
if e.response['Error']['Code'] == 'InvalidKmsID.NotFound':
self.warn("aws customer master key {0} was deleted from outside nixops,"
" it needs to be recreated...".format(self._state["keyId"]))
self.cleanup_state()
return
cmk_state = cmk['KeyMetadata']['KeyState']
if cmk_state == "Enabled":
return
elif cmk_state == "Disabled":
raise Exception("aws customer master key state is {1}, Enable it form the console".format(cmk_state))
elif cmk_state == "PendingDeletion":
raise Exception("aws customer master key state is {1}, run a destroy operation to sync the state or cancel the deletion.".format(cmk_state))
else:
raise Exception("aws customer master key state is {1}".format(cmk_state))

def _destroy(self):
if self.state != self.UP: return

self.get_client(service="kms").delete_alias(AliasName="alias/" + self._state['alias'])
self.log("scheduling aws customer master key `{0}` deletion to {1} day(s)..."
.format(self._state['alias'], self._state['deletionWaitPeriod']))
try:
self.get_client(service="kms").schedule_key_deletion(
KeyId=self._state['keyId'],
PendingWindowInDays=self._state['deletionWaitPeriod'])
except botocore.exceptions.ClientError as e:
if e.response['Error']['Code'] == 'InvalidCmkID.NotFound':
self.warn("aws customer master key {0} was already deleted".format(self._state['keyId']))
else:
raise e
self.cleanup_state()

def cleanup_state(self):
with self.depl._db:
self.state = self.MISSING
self._state['keyId'] = None
self._state['region'] = None
self._state['alias'] = None
self._state['policy'] = None
self._state['description'] = None
self._state['origin'] = None
self._state['customKeyStoreId'] = None
self._state['deletionWaitPeriod'] = None

def destroy(self, wipe=False):
self._destroy()
return True
Loading