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
17 changes: 15 additions & 2 deletions README.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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 <http://docs.aws.amazon.com/STS/latest/APIReference/API_AssumeRole.html>`_.
subject to their permissions, with `sts:AssumeRole <http://docs.aws.amazon.com/STS/latest/APIReference/API_AssumeRole.html>`_.
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
Expand Down
112 changes: 101 additions & 11 deletions keymaker/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
import codecs
import grp
import shlex
import yaml
from collections import namedtuple

import boto3
Expand All @@ -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:])
Expand All @@ -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)
Expand Down Expand Up @@ -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()
Expand All @@ -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"],
Expand Down Expand Up @@ -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)
Expand All @@ -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):
Expand Down Expand Up @@ -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)
Expand Down