Example #1
0
class Store():    
    def __init__(self, logger=None):
        self.logger         = CustomLogger().logger if logger is None else logger
        self.certs_location = '/etc/letsencrypt/live'
        self.endpoint_url   = os.getenv('ENDPOINT_URL')
        self.aws_access_key = os.getenv('AWS_ACCESS_KEY')
        self.aws_secret_key = os.getenv('AWS_SECRET_KEY')
        self.aws_region     = os.getenv('AWS_REGION')
        self.s3_bucket_name = os.getenv('CERTS_BUCKET_NAME')
        self.client         = self._client()
        
    
    def _client(self):
        try:
            return  boto3.client('s3', 
                        endpoint_url=self.endpoint_url, 
                        aws_access_key_id=self.aws_access_key, 
                        aws_secret_access_key=self.aws_secret_key, 
                        region_name=self.aws_region)
        except Exception:
            self.logger.exception('Can not in create s3 client')
            return None

    def _calcSHA256(self, filepath):
        sha256_hash = hashlib.sha256()
        with open(filepath,'rb') as f:
            # Read and update hash string value in blocks of 4K
            for byte_block in iter(lambda: f.read(4096),b''):
                sha256_hash.update(byte_block)
            return sha256_hash.hexdigest()                               

    def getMetaData(self, object_key):
        """Get the certificate metadata"""
        resp = self.client.head_object(Bucket=self.s3_bucket_name, Key='{0}/metadata.json'.format(object_key))
        if 'Metadata' not in resp:
            return None
        
        return resp['Metadata']
    
    def saveCerts(self):
        """ Saves the letsencrypt certificates files to a s3-compatible object storage"""
        certs_files = {}
        if self.client is None:
            self.logger.error('No s3 client initialized')
            return
        for cert in os.listdir(self.certs_location):
            cert_location = os.path.join(self.certs_location, cert)
            if os.path.isdir(cert_location):
                certs_files[cert] = {}
                cert_files = list(filter(lambda filename: all(ex_str not in filename.lower() for ex_str in ['readme', 'metadata']), os.listdir(cert_location)))
                for file in cert_files:
                    filepath   = os.path.join(cert_location, file)
                    filesha256 = self._calcSHA256(filepath)
                    cert_key = os.path.splitext(file)[0]
                    certs_files[cert][cert_key] = filesha256
                    # Save the certificates to a bucket
                    try:
                        with open(filepath, 'rb') as certdata:
                            self.client.put_object(
                                ACL='private',
                                Body=certdata,
                                Bucket=self.s3_bucket_name,
                                Key='{0}/{1}'.format(cert, file))
                    except Exception:
                        self.logger.error('Can not save the %s certificate file' % cert)

                # create and upload a metadata file contains the certificates files sha256         
                metadata_file = os.path.join(cert_location, 'metadata.json')
                metadata_obj  = json.dumps(certs_files[cert], indent=4)
                try:
                    with open(metadata_file, 'w') as f:
                        f.write(metadata_obj)
                except Exception:
                    self.logger.error('Can not save the metadata json file for %s certificate' % cert)
                    return

                if os.path.isfile(metadata_file):
                    self.client.put_object(
                        ACL='private',
                        Body=metadata_obj,
                        Bucket=self.s3_bucket_name,
                        Key='{0}/{1}'.format(cert, 'metadata.json'),
                        Metadata=certs_files[cert])

        self.logger.info('certificates files saved to %s bucket' % self.s3_bucket_name)      
Example #2
0
class AcmeOperation:
    load_dotenv()

    def __init__(self, provider=None):
        """
        Automate certbot and lexicon to obtain and store 
        let's encrypt ssl certificates into S3-compatible object storage
        """
        self.logger = CustomLogger().logger
        self.dns_provider = provider
        self.dns_provider_username = os.getenv('DNS_PROVIDER_USERNAME')
        self.dns_provider_auth_token = os.getenv('DNS_PROVIDER_AUTH_TOKEN')
        self.client_ip_address = self._getPublicIP()
        self.dns_provider_update_delay = 30
        self.config = Config(logger=self.logger)
        self.s3_store = Store(logger=self.logger)
        self.test = False

    def _providerCheck(self):
        if self.dns_provider in self.config.getconfig['domains']:
            if len(self.config.getconfig['domains'][self.dns_provider]) > 0:
                return True
            else:
                self.logger.error('no domains stored for {0}'.format(
                    self.dns_provider))
                return False

        self.logger.error("provider is not in the domains list")
        return False

    def _getPublicIP(self):
        return urllib.request.urlopen('https://api.ipify.org/').read().decode(
            'utf8')

    def _getToolPath(self, tool):
        if tool is not None and tool not in self.config.getconfig:
            return None
        if 'path' in self.config.getconfig[tool]:
            return self.config.getconfig[tool]['path']

    def _runCmd(self, args):
        result = Result()

        process = Popen(args, stdout=PIPE, stderr=PIPE)
        stdout, stderr = process.communicate()

        result.stdout = stdout
        result.stderr = stderr

        if process.returncode:
            self.logger.error('Error executing command {0}'.format(args))
            self.logger.error('StdErr: {0}'.format(stderr))

        return result

    def obtain(self, test=False, expand=False):
        """
        Obtains the initial letsencrypt certificates for specific domain name provider 
        using manual script hooks to validate the ownership of the domains

        Certbot cli cmd generated:
        certbot certonly --manual --preferred-challenges=dns 
        --manual-auth-hook "/path/to/acmebot.py auth -p namecheap" 
        --manual-cleanup-hook "/path/to/acmebot.py cleanup -p namecheap" 
        -d example.com -d sub2.example.com -d another-example.com 
        --manual-public-ip-logging-ok --noninteractive --agree-tos --test-cert
        """
        hook_file = os.path.join(os.path.dirname(os.path.realpath(__file__)),
                                 'acmebot')
        if self._providerCheck():
            certbot = self._getToolPath('certbot')

            if certbot is None:
                self.logger.error(
                    "failed to run the certbot cmd, certbot path is not set")
                return

            args = [
                certbot, 'certonly', '--manual', '--preferred-challenges',
                'dns'
            ]

            args.extend([
                '--manual-auth-hook',
                quote("{0} auth -p {1}".format(hook_file, self.dns_provider))
            ])
            args.extend([
                '--manual-cleanup-hook',
                quote("{0} cleanup -p {1}".format(hook_file,
                                                  self.dns_provider))
            ])
            for domain in self.config.getconfig['domains'][self.dns_provider]:
                args.extend(['-d', domain])

            # adding certbot options to run in a non interactive mode
            args.extend([
                '--manual-public-ip-logging-ok', '--noninteractive',
                '--agree-tos', '--quiet'
            ])

            if test:
                args.append('--test-cert')

            if expand:
                args.append('--expand')

            certbot_cmd = ' '.join(args)
            # for some reason using Popen does not work with certbot
            # so we are using os.system for now
            # TODO: figure our if we can use subprocess.Popen
            os.system(certbot_cmd)

    def hook(self, action=None):
        # the passed environment variables from certbot
        CERTBOT_DOMAIN = os.environ.get("CERTBOT_DOMAIN", None)
        CERTBOT_VALIDATION = os.environ.get("CERTBOT_VALIDATION", None)

        cmd_index = 6
        lexicon = self._getToolPath('lexicon')

        if lexicon is None:
            self.logger.error(
                "failed to run the lexicon cmd, lexicon path is not set")
            return

        args = [
            lexicon, self.dns_provider,
            '--auth-usernam={0}'.format(self.dns_provider_username),
            '--auth-token={0}'.format(self.dns_provider_auth_token),
            '--auth-client-ip={0}'.format(self.client_ip_address), '--ttl=100',
            CERTBOT_DOMAIN, 'TXT', '--name',
            '_acme-challenge.{0}'.format(CERTBOT_DOMAIN), '--content',
            CERTBOT_VALIDATION
        ]
        if action == 'auth':
            #   https://github.com/AnalogJ/lexicon/blob/master/examples/certbot.default.sh#L46
            #   How many seconds to wait after updating your DNS records. This may be required,
            #   depending on how slow your DNS host is to begin serving new DNS records after updating
            #   them via the API. 30 seconds is a safe default, but some providers can be very slow
            #   (e.g. Linode).
            #
            #   Defaults to 30 seconds
            args.insert(cmd_index, 'create')
            self._runCmd(args)
            time.sleep(self.dns_provider_update_delay)
            # now save the created certificates to s3-compatible object storage
            self.s3_store.saveCerts()
        elif action == 'cleanup':
            args.insert(cmd_index, 'delete')
            self._runCmd(args)

    def manual_s3_upload(self):
        """manually uploads the live certificate files into the configured s3-compatible storage"""
        try:
            self.s3_store.saveCerts()
        except Exception as e:
            self.logger.error(
                'Failed to manually upload the certificates to the s3-compatible storage, Reason: %s'
                % e)