Skip to content

Commit

Permalink
Adding self signed certificates generation (#9)
Browse files Browse the repository at this point in the history
  • Loading branch information
JohnPreston authored Aug 5, 2021
1 parent d1c9b89 commit 3449ad7
Show file tree
Hide file tree
Showing 10 changed files with 834 additions and 564 deletions.
460 changes: 269 additions & 191 deletions ecs-files-input.json

Large diffs are not rendered by default.

153 changes: 153 additions & 0 deletions ecs_files_composer/aws_mgmt.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,153 @@
# -*- coding: utf-8 -*-
# SPDX-License-Identifier: MPL-2.0
# Copyright 2020-2021 John Mille<[email protected]>

"""AWS module."""

import re

import boto3
from boto3 import session
from botocore.exceptions import ClientError

from ecs_files_composer import input
from ecs_files_composer.common import LOG
from ecs_files_composer.envsubst import expandvars


def create_session_from_creds(tmp_creds, region=None):
"""
Function to easily convert the AssumeRole reply into a boto3 session
:param tmp_creds:
:return:
:rtype boto3.session.Session
"""
creds = tmp_creds["Credentials"]
params = {
"aws_access_key_id": creds["AccessKeyId"],
"aws_secret_access_key": creds["SecretAccessKey"],
"aws_session_token": creds["SessionToken"],
}
if region:
params["region_name"] = region
return boto3.session.Session(**params)


class AwsResourceHandler(object):
"""
Class to handle all AWS related credentials init.
"""

def __init__(self, role_arn=None, external_id=None, region=None, iam_config_object=None):
"""
:param str role_arn:
:param str external_id:
:param str region:
:param ecs_files_composer.input.IamOverrideDef iam_config_object:
"""
self.session = session.Session()
self.client_session = session.Session()
if role_arn or iam_config_object:
if role_arn and not iam_config_object:
params = {"RoleArn": role_arn, "RoleSessionName": "EcsConfigComposer@AwsResourceHandlerInit"}
if external_id:
params["ExternalId"] = external_id
tmp_creds = self.session.client("sts").assume_role(**params)
self.client_session = create_session_from_creds(tmp_creds, region=region)
elif iam_config_object:
params = {
"RoleArn": iam_config_object.role_arn,
"RoleSessionName": f"{iam_config_object.session_name}@AwsResourceHandlerInit",
}
if iam_config_object.external_id:
params["ExternalId"] = iam_config_object.external_id
tmp_creds = self.session.client("sts").assume_role(**params)
self.client_session = create_session_from_creds(tmp_creds, region=iam_config_object.region_name)


class S3Fetcher(AwsResourceHandler):
"""
Class to handle S3 actions
"""

def __init__(self, role_arn=None, external_id=None, region=None, iam_config_object=None):
super().__init__(role_arn, external_id, region, iam_config_object)
self.client = self.client_session.client("s3")

def get_content(self, s3_uri=None, s3_bucket=None, s3_key=None):
"""
Retrieves a file in a temp dir and returns content
:param str s3_uri:
:param str s3_bucket:
:param str s3_key:
:return: The Stream Body for the file, allowing to do various things
"""
bucket_re = re.compile(r"(?:s3://)(?P<bucket>[a-z0-9-.]+)/(?P<key>[\S]+$)")
if s3_uri and bucket_re.match(s3_uri).groups():
s3_bucket = bucket_re.match(s3_uri).group("bucket")
s3_key = bucket_re.match(s3_uri).group("key")
try:
file_r = self.client.get_object(Bucket=s3_bucket, Key=s3_key)
file_content = file_r["Body"]
return file_content
except self.client.exceptions.NoSuchKey:
LOG.error(f"Failed to download the file {s3_key} from bucket {s3_bucket}")
raise


class SsmFetcher(AwsResourceHandler):
"""
Class to handle SSM actions
"""

def __init__(self, role_arn=None, external_id=None, region=None, iam_config_object=None):
super().__init__(role_arn, external_id, region, iam_config_object)
self.client = self.client_session.client("ssm")

def get_content(self, parameter_name):
"""
Import the Content of a given parameter
:param parameter_name:
:return:
"""
try:
parameter = self.client.get_parameter(Name=parameter_name, WithDecryption=True)
return parameter["Parameter"]["Value"]
except self.client.exceptions:
raise
except ClientError:
raise


class SecretFetcher(AwsResourceHandler):
"""
Class to handle Secret Manager actions
"""

def __init__(self, role_arn=None, external_id=None, region=None, iam_config_object=None):
super().__init__(role_arn, external_id, region, iam_config_object)
self.client = self.client_session.client("secretsmanager")

def get_content(self, secret):
"""
Import the Content of a given parameter
:param input.SecretDef secret:
:return:
"""
secret_id = expandvars(secret.secret_id)
params = {"SecretId": secret_id}
LOG.debug(f"Retrieving secretsmanager://{secret_id}")
if secret.version_id:
params["VersionId"] = secret.version_id
if secret.version_stage:
params["VersionStage"] = secret.version_stage
try:
parameter = self.client.get_secret_value(**params)
return parameter["SecretString"]
except self.client.exceptions:
raise
except ClientError:
raise
113 changes: 113 additions & 0 deletions ecs_files_composer/certificates_mgmt.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
# -*- coding: utf-8 -*-
# SPDX-License-Identifier: MPL-2.0
# Copyright 2020-2021 John Mille<[email protected]>

import pathlib
import socket
from os import path
from typing import Any

from OpenSSL import crypto

from ecs_files_composer.files_mgmt import File
from ecs_files_composer.input import X509CertDef


class X509Certificate(X509CertDef, object):
"""
Class to wrap actions around a new X509 certificate
"""

def __init__(self, **data: Any):
super().__init__(**data)
self.key = None
self.cert = None
self.key_content = None
self.cert_content = None
self.cert_file = None
self.key_file = None
self.cert_file_path = None
self.key_file_path = None

def init_cert_paths(self):
self.cert_file_path = path.abspath(f"{self.dir_path}/{self.cert_file_name}")
self.key_file_path = path.abspath(f"{self.dir_path}/{self.key_file_name}")
print(f"Creating {self.dir_path} folder")
dir_path = pathlib.Path(path.abspath(self.dir_path))
dir_path.mkdir(parents=True, exist_ok=True)

def generate_key(self):
self.key = crypto.PKey()
self.key.generate_key(crypto.TYPE_RSA, 4096)

def set_common_name(self):
if self.common_name is None:
self.common_name = socket.gethostname()

def generate_cert(self):
if not self.common_name:
self.set_common_name()
self.cert = crypto.X509()
self.cert.get_subject().C = self.country_name
self.cert.get_subject().ST = self.state_or_province_name
self.cert.get_subject().L = self.locality_name
self.cert.get_subject().O = self.organization_name
self.cert.get_subject().OU = self.organization_unit_name
self.cert.get_subject().CN = self.common_name
self.cert.get_subject().emailAddress = self.email_address
self.cert.set_serial_number(0)
self.cert.gmtime_adj_notBefore(0)
self.cert.gmtime_adj_notAfter(int(self.validity_end_in_seconds))
self.cert.set_issuer(self.cert.get_subject())
self.cert.set_pubkey(self.key)
self.cert.sign(self.key, 'sha512')

def generate_cert_content(self):
if not self.key:
self.generate_key()
if not self.cert:
self.generate_cert()
self.cert_content = crypto.dump_certificate(crypto.FILETYPE_PEM, self.cert).decode("utf-8")
self.key_content = crypto.dump_privatekey(crypto.FILETYPE_PEM, self.key).decode("utf-8")

def set_cert_files(self):
if not self.cert_content or not self.key_content:
self.generate_cert_content()
self.key_file = File().parse_obj(
{
"content": self.key_content,
"path": self.key_file_path,
"mode": "0600",
"owner": self.owner,
"group": self.group,
}
)
self.cert_file = File().parse_obj(
{
"content": self.cert_content,
"path": self.cert_file_path,
"mode": "0600",
"owner": self.owner,
"group": self.group,
}
)


def process_x509_certs(job):
"""
:param ecs_files_composer.input.Model job:
:return:
"""
if not hasattr(job.certificates, "x509") or not job.certificates.x509:
return
for cert_path, cert_def in job.certificates.x509.items():
cert_obj = X509Certificate(
keyFileName=cert_def["keyFileName"], certFileName=cert_def["certFileName"]
).parse_obj(cert_def)
cert_obj.dir_path = cert_path
cert_obj.init_cert_paths()
cert_obj.set_cert_files()
job.certificates.x509[cert_path] = cert_obj
job.files[cert_obj.cert_file.path] = cert_obj.cert_file
job.files[cert_obj.key_file_path] = cert_obj.key_file
34 changes: 1 addition & 33 deletions ecs_files_composer/common.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,39 +6,7 @@
import sys
from os import environ


def keyisset(x, y):
"""
Macro to figure if the the dictionary contains a key and that the key is not empty
:param x: The key to check presence in the dictionary
:type x: str
:param y: The dictionary to check for
:type y: dict
:returns: True/False
:rtype: bool
"""
if isinstance(y, dict) and x in y.keys() and y[x]:
return True
return False


def keypresent(x, y):
"""
Macro to figure if the the dictionary contains a key and that the key is not empty
:param x: The key to check presence in the dictionary
:type x: str
:param y: The dictionary to check for
:type y: dict
:returns: True/False
:rtype: bool
"""
if isinstance(y, dict) and x in y.keys():
return True
return False
from compose_x_common.compose_x_common import keyisset


def setup_logging():
Expand Down
Loading

0 comments on commit 3449ad7

Please sign in to comment.