diff --git a/README.rst b/README.rst index d63fc5e..ab89d6c 100644 --- a/README.rst +++ b/README.rst @@ -70,16 +70,29 @@ Cross-account authentication Some AWS security models put IAM users in one AWS account, and resources (EC2 instances, S3 buckets, etc.) in a family of other federated AWS accounts (for example, a dev account and a prod account). Users then assume roles in those federated accounts, -subject to their permissions, with `sts:AssumeRole `_. +subject to their permissions, with `sts:AssumeRole `_. When users connect via SSH to instances running in federated accounts, Keymaker can be instructed to look up the user identity and SSH public key in the other AWS account (called the "ID resolver" account). -Keymaker expects to find this configuration information by introspecting the instance's own IAM role description. The +Keymaker can find its configuration information into two different places : +- first by introspecting the instance's own IAM role description. The description is expected to contain a list of space-separated config tokens, for example, ``keymaker_id_resolver_account=123456789012 keymaker_id_resolver_role=id_resolver``. For ``sts:AssumeRole`` to work, the role ``id_resolver`` in account 123456789012 is expected to have a trust policy allowing the instance's IAM role to perform sts:AssumeRole on ``id_resolver``. +- then if role description does not contain any configuration tokens, Keymaker will search config-token in the following yaml file : `/etc/keymaker/keymaker.yaml`. + +Below an example: + +```yaml +--- +keymaker_id_resolver_account: "123456789" +keymaker_id_resolver_iam_role: "anIamRole" +keymaker_require_iam_group: "myGroup" +keymaker_linux_group_prefix: "myPrefix" +``` + Run the following command in the ID resolver account (that contains the IAM users) to apply this configuration automatically: ``keymaker configure --instance-iam-role arn:aws:iam::987654321098:role/ROLE_NAME --cross-account-profile AWS_CLI_PROFILE_NAME``. Here, 987654321098 is the account ID of the federated account where EC2 instances will run, and AWS_CLI_PROFILE_NAME diff --git a/keymaker/__init__.py b/keymaker/__init__.py index d801b3f..92d919c 100644 --- a/keymaker/__init__.py +++ b/keymaker/__init__.py @@ -14,6 +14,7 @@ import codecs import grp import shlex +import yaml from collections import namedtuple import boto3 @@ -33,6 +34,7 @@ def __str__(self): ARN.__new__.__defaults__ = ("aws", "", "", "", "") iam_linux_group_prefix = "keymaker_" +config_file_path = "/etc/keymaker/keymaker.yaml" def parse_arn(arn): return ARN(*arn.split(":", 5)[1:]) @@ -48,7 +50,7 @@ def ensure_iam_role(iam, role_name, trust_principal, keymaker_config=None): else: logger.info("Creating IAM role %s", role_name) role = iam.create_role(RoleName=role_name, AssumeRolePolicyDocument=json.dumps(trust_policy)) - role_config = parse_keymaker_config(role.description) + role_config = parse_keymaker_config(role) if keymaker_config is not None and role_config != keymaker_config: description = ", ".join("=".join(i) for i in keymaker_config.items()) if keymaker_config else "" logger.info('Updating IAM role description to "%s"', description) @@ -102,18 +104,43 @@ def configure(args): logger.info("Attaching IAM policy %s to IAM role %s", keymaker_policy, instance_role) instance_role.attach_policy(PolicyArn=keymaker_policy.arn) -def parse_keymaker_config(iam_role_description): + +def parse_keymaker_config_in_file(config_file_path): config = {} - for role_desc_word in re.split("[\s\,]+", iam_role_description or ""): - if role_desc_word.startswith("keymaker_") and role_desc_word.count("=") == 1: - config.update([shlex.split(role_desc_word)[0].split("=")]) + try: + with open(config_file_path, 'r') as stream: + config = yaml.load(stream) + except Exception as e: + logger.warn(str(e)) + return config + +def parse_keymaker_config(iam_role): + config = {} + iam_role_description = None + if 'Description' in iam_role: + iam_role_description = iam_role['Description'] + elif 'description' in iam_role: + iam_role_description = iam_role['description'] + + if iam_role_description is not None: + for role_desc_word in re.split("[\s\,]+", iam_role_description or ""): + if role_desc_word.startswith("keymaker_") and role_desc_word.count("=") == 1: + config.update([shlex.split(role_desc_word)[0].split("=")]) + + if len(config) == 0 and os.path.isfile(config_file_path) : + config = parse_keymaker_config_in_file(config_file_path) + + return config + + def get_assume_role_session(sts, role_arn): credentials = sts.assume_role(RoleArn=str(role_arn), RoleSessionName=__name__)["Credentials"] - return boto3.Session(aws_access_key_id=credentials["AccessKeyId"], + sess = boto3.Session(aws_access_key_id=credentials["AccessKeyId"], aws_secret_access_key=credentials["SecretAccessKey"], aws_session_token=credentials["SessionToken"]) + return sess def get_authorized_keys(args): session = boto3.Session() @@ -123,8 +150,9 @@ def get_authorized_keys(args): try: role_arn = parse_arn(sts.get_caller_identity()["Arn"]) _, role_name, instance_id = role_arn.resource.split("/", 2) - config = parse_keymaker_config(iam.get_role(RoleName=role_name)["Role"]["Description"]) + config = parse_keymaker_config(iam.get_role(RoleName=role_name)["Role"]) except Exception as e: + logger.warn('exception in get_authorized_keys') logger.warn(str(e)) if "keymaker_id_resolver_account" in config: id_resolver_role_arn = ARN(service="iam", account=config["keymaker_id_resolver_account"], @@ -164,7 +192,27 @@ def aws_to_unix_id(aws_key_id): return 2000 + (int.from_bytes(uid_bytes, byteorder=sys.byteorder) // 2) def get_uid(args): - iam = boto3.resource("iam") + + session = boto3.Session() + iam_caller = session.client("iam") + sts = session.client("sts") + config = {} + + try: + role_arn = parse_arn(sts.get_caller_identity()["Arn"]) + _, role_name, instance_id = role_arn.resource.split("/", 2) + config = parse_keymaker_config(iam_caller.get_role(RoleName=role_name)["Role"]) + except Exception as e: + logger.warn(str(e)) + + if "keymaker_id_resolver_account" in config: + id_resolver_role_arn = ARN(service="iam", account=config["keymaker_id_resolver_account"], + resource="role/" + config["keymaker_id_resolver_iam_role"]) + iam_resource = get_assume_role_session(sts, id_resolver_role_arn).resource("iam") + + else: + iam_resource = boto3.resource("iam") + try: user_id = iam.User(args.user).user_id uid = aws_to_unix_id(user_id) @@ -173,7 +221,28 @@ def get_uid(args): err_exit("Error while retrieving UID for {u}: {e}".format(u=args.user, e=str(e)), code=os.errno.EINVAL) def get_groups(args): - iam = boto3.resource("iam") + + session = boto3.Session() + iam_caller = session.client("iam") + sts = session.client("sts") + config = {} + try: + role_arn = parse_arn(sts.get_caller_identity()["Arn"]) + _, role_name, instance_id = role_arn.resource.split("/", 2) + config = parse_keymaker_config(iam_caller.get_role(RoleName=role_name)["Role"]) + except Exception as e: + logger.warn(str(e)) + if "keymaker_id_resolver_account" in config: + id_resolver_role_arn = ARN(service="iam", account=config["keymaker_id_resolver_account"], + resource="role/" + config["keymaker_id_resolver_iam_role"]) + iam_resource = get_assume_role_session(sts, id_resolver_role_arn).resource("iam") + + else: + iam_resource = boto3.resource("iam") + + if 'keymaker_linux_group_prefix' in config: + iam_linux_group_prefix = config['keymaker_linux_group_prefix'] + try: for group in iam.User(args.user).groups.all(): if group.name.startswith(iam_linux_group_prefix): @@ -318,8 +387,29 @@ def is_managed(unix_username): def sync_groups(args): from pwd import getpwnam - iam = boto3.resource("iam") - for group in iam.groups.all(): + + session = boto3.Session() + iam_caller = session.client("iam") + sts = session.client("sts") + config = {} + try: + role_arn = parse_arn(sts.get_caller_identity()["Arn"]) + _, role_name, instance_id = role_arn.resource.split("/", 2) + config = parse_keymaker_config(iam_caller.get_role(RoleName=role_name)["Role"]) + except Exception as e: + logger.warn(str(e)) + if "keymaker_id_resolver_account" in config: + id_resolver_role_arn = ARN(service="iam", account=config["keymaker_id_resolver_account"], + resource="role/" + config["keymaker_id_resolver_iam_role"]) + iam_resource = get_assume_role_session(sts, id_resolver_role_arn).resource("iam") + + else: + iam_resource = boto3.resource("iam") + + if 'keymaker_linux_group_prefix' in config: + iam_linux_group_prefix = config['keymaker_linux_group_prefix'] + + for group in iam_resource.groups.all(): if not group.name.startswith(iam_linux_group_prefix): continue logger.info("Syncing IAM group %s", group.name)