diff --git a/deployments/aws/bin/veraison b/deployments/aws/bin/veraison index 5045c088..31b44b94 100755 --- a/deployments/aws/bin/veraison +++ b/deployments/aws/bin/veraison @@ -5,37 +5,46 @@ import argparse import asyncio import getpass import inspect -import io import logging import os import pprint +import random import re import shutil import socket import stat +import string import sys -import tarfile import time from asyncio.subprocess import Process, PIPE from copy import copy +from datetime import datetime, timedelta, timezone from urllib.parse import urlparse -import ar import boto3 import fabric import requests import xdg.BaseDirectory import yaml from botocore.exceptions import ClientError +from cryptography import x509 +from cryptography.hazmat.primitives import hashes, serialization +from cryptography.hazmat.primitives.asymmetric import ec +from cryptography.x509.oid import NameOID +from envsubst import envsubst from sqlitedict import SqliteDict + COLOR_DARK_GREY = '\x1b[38;5;245m' +COLOR_MEDIUM_GREY = '\x1b[38;5;250m' COLOR_GREY = '\x1b[38;20m' COLOR_GREEN = '\x1b[38;5;2m' COLOR_YELLOW = '\x1b[33;20m' COLOR_RED = '\x1b[31;20m' COLOR_BOLD_RED = '\x1b[31;1m' COLOR_RESET = '\x1b[0m' +']]]]]]]]' # "close" the brackets above to fix nvim's auto-indent + class Aws: @@ -68,6 +77,13 @@ class Aws: self.close() +def randomword(n=32, use_punctuation=False): + alphabet = string.ascii_letters + string.digits + if use_punctuation: + alphabet += string.punctuation + return ''.join(random.choice(alphabet) for _ in range(n)) + + def get_public_ip_address(): resp = requests.get('http://ifconfig.me') if resp.status_code != 200: @@ -125,7 +141,60 @@ def revoke_security_group_rules_by_tag(aws, group_id, tag): ) -def update_dynamic_address_rules(aws, deployment_name, tag, ports): +def command_create_packer_security_group(cmd, deployment_name): + vpc_id = cmd.cache.get('vpc-id') + if not vpc_id: + cmd.fail('vpc-id has not been configured') + + cmd.logger.debug('creating Packer security group...') + resp = cmd.aws.ec2.create_security_group( + GroupName='veraison-packer', + Description='Temporary security group for creating Veraison packer imeages', + VpcId=vpc_id, + TagSpecifications=[ + { + 'ResourceType': 'security-group', + 'Tags': [ + {'Key': 'veraison-deployment', 'Value': deployment_name}, + {'Key': 'Class', 'Value': 'packer'}, + ], + } + ], + ) + + cmd.logger.debug('obtaining public IP address for localhost...') + my_addr = get_public_ip_address() + + cmd.logger.debug('Adding access rule for SSH from localhost...') + authorize_ports_for_address( + cmd.aws, resp['GroupId'], my_addr, [22], cmd.cache.user_tag, + deployment_tags=[ + {'Key': 'veraison-deployment', 'Value': deployment_name}, + {'Key': 'Class', 'Value': 'packer'}, + ], + ) + + +def command_delete_packer_security_group(cmd, deployment_name): + cmd.logger.debug(f'looking for packer group(s) for deployment {deployment_name}...') + resp = cmd.aws.ec2.describe_security_groups( + Filters=[ + {'Name': 'tag:veraison-deployment', 'Values': [deployment_name]}, + {'Name': 'tag:Class', 'Values': ['packer']}, + ] + ) + + group_ids = [sgr['GroupId'] for sgr in resp['SecurityGroups']] + if not group_ids: + cmd.logger.debug(f'no packer group found for deployment {deployment_name}') + return + + for group_id in group_ids: + cmd.logger.debug(f'deleting security group {group_id}...') + cmd.aws.ec2.delete_security_group(GroupId=group_id) + + +def command_update_dynamic_address_rules(cmd, deployment_name, tag, ports): resp = aws.ec2.describe_security_groups( Filters=[{ 'Name': 'tag:veraison-deployment', @@ -133,35 +202,52 @@ def update_dynamic_address_rules(aws, deployment_name, tag, ports): }] ) + group_ids = [sgr['GroupId'] for sgr in resp['SecurityGroups']] if not group_ids: + cmd.logger.info(f'no groups found for deployment {deployment_name}') return my_addr = get_public_ip_address() - for group_id in group_ids: - revoke_security_group_rules_by_tag(aws, group_id, tag) + revoke_security_group_rules_by_tag(cmd.aws, group_id, tag) authorize_ports_for_address( - aws, group_id, my_addr, ports, tag, + cmd.aws, group_id, my_addr, ports, tag, deployment_tags=[{'Key': 'veraison-deployment', 'Value': deployment_name}]) -def get_deployment_info(aws, deployment_name): - resp = aws.cf.describe_stack_resources(StackName=deployment_name) - stack_resources = {sr['LogicalResourceId']: sr for sr in resp['StackResources']} +def get_stack_instances_info(aws, stack_name): + resp = aws.cf.describe_stack_resources(StackName=stack_name) - instance_id = stack_resources['VeraisonInstance']['PhysicalResourceId'] - resp = aws.ec2.describe_instances(InstanceIds=[instance_id]) - instance = resp['Reservations'][0]['Instances'][0] - pub_iface = instance['NetworkInterfaces'][0]['Association'] + info = {} + for resource in resp['StackResources']: + if resource['ResourceType'] != 'AWS::EC2::Instance': + continue + instance_id = resource['PhysicalResourceId'] + resp = aws.ec2.describe_instances(InstanceIds=[instance_id]) + instance = resp['Reservations'][0]['Instances'][0] - return { - 'instance': { - 'id': instance_id, - 'dns_name': pub_iface["PublicDnsName"], - 'ip_address': pub_iface["PublicIp"], - }, - } + instance_name = None + fallback_name = instance_id + for tag in instance['Tags']: + if tag['Key'] == 'deployment-instance-name': + instance_name = tag['Value'] + break + elif tag['Key'] == 'Name': + fallback_name = tag['Value'] + + if not instance_name: + instance_name = fallback_name + + pub_iface = instance['NetworkInterfaces'][0]['Association'] + + info[instance_name] = { + 'id': instance_id, + 'dns_name': pub_iface["PublicDnsName"], + 'ip_address': pub_iface["PublicIp"], + } + + return info def get_ami_id(aws, name): @@ -186,7 +272,7 @@ def run_in_shell(cmd, should_log): async def _run_in_shell_teed(cmd, logger): process: Process = await asyncio.create_subprocess_shell( cmd, stdout=PIPE, stderr=PIPE, cwd=os.getcwd()) - + stdout_buf, stderr_buf = [], [] tasks = { @@ -210,82 +296,149 @@ async def _run_in_shell_teed(cmd, logger): return rc, ''.join(stdout_buf), ''.join(stderr_buf) -def command_get_vpc_id(cmd, args): - if args.vpc_id: - cmd.logger.debug(f'writing {args.vpc_id} to cache') - cmd.cache['vpc_id'] = args.vpc_id - return args.vpc_id +def command_get_config(cmd, name): + try: + return cmd.cache[name] + except KeyError: + cmd.fail(f'could not get {name} from cache; has configure command been run?') - vpc_id = cmd.cache.get('vpc_id') - if vpc_id: - cmd.logger.debug(f'using VPC ID from cache: {vpc_id}') - return vpc_id - cmd.logger.debug('no VPC ID specified; trying to identify from account...') - resp = cmd.aws.ec2.describe_vpcs( - Filters=[{ - 'Name': 'state', - 'Values': ['available'], - }] - ) - if len(resp['Vpcs']) == 1: - vpc_id = resp['Vpcs'][0]['VpcId'] - cmd.cache['vpc_id'] = vpc_id - return vpc_id - elif len(resp['Vpcs']) > 1: - vpc_ids = ', '.join(vpc['VpcId'] for vpc in resp['Vpcs']) - cmd.fail(f'multiple VPCs found: {vpc_ids}; use --vpc-id to specify ' - 'which one should be used') - else: - cmd.fail('no VPCs found in the account') +def command_instantiate_template(cmd, deployment_name, src_path): + with open(src_path) as fh: + instantiated_template_body = envsubst(fh.read()) + basename = os.path.basename(src_path).removesuffix('.template') + out_path = f'/tmp/{deployment_name}-{basename}' + cmd.logger.debug(f'writing {out_path}') + with open(out_path, 'w') as wfh: + wfh.write(instantiated_template_body) -def command_get_subnet_id(cmd, args): - if args.subnet_id: - cmd.logger.debug(f'writing {args.subnet_id} to cache') - cmd.cache['subnet_id'] = args.subnet_id - return args.subnet_id + return out_path - subnet_id = cmd.cache.get('subnet_id') - if subnet_id: - cmd.logger.debug(f'using subnet ID from cache: {subnet_id}') - return subnet_id - cmd.logger.debug('no subnet ID specified; trying to identify from account...') - resp = cmd.aws.ec2.describe_subnets( - Filters=[{ - 'Name': 'state', - 'Values': ['available'], - }] +def command_create_image(cmd, args, name, packer_params=None): + if not shutil.which('packer'): + cmd.fail('packer must be installed on the system') + + if not os.path.isfile(args.template): + cmd.fail(f'template {args.template} does not exist') + + full_name = f'{args.deployment_name}-{name}' + cmd.logger.info(f'creating image: {full_name}...') + + command_create_packer_security_group(cmd, args.deployment_name) + + try: + cmd.logger.debug('checking for existing AMI with that name...') + existing_id = get_ami_id(cmd.aws, full_name) + if existing_id: + if not args.force: + cmd.fail(f'image {full_name} already exits (use -f to overwrite)') + cmd.logger.info('removing existing image...') + cmd.aws.ec2.deregister_image(ImageId=existing_id) + + cmd.logger.info('building using packer...') + vpc_id = command_get_config(cmd, 'vpc-id') + subnet_id = command_get_config(cmd, 'subnet-id') + region = command_get_config(cmd, 'region') + + params_dict = { + 'ami_name': full_name, + 'deployment_name': args.deployment_name, + 'instance_type': args.instance_type, + 'region': region, + 'vpc_id': vpc_id, + 'subnet_id': subnet_id, + } + params_dict.update(packer_params or {}) + + params_str = ' '.join(f'-var {k}={v}' for k, v in params_dict.items() if v is not None) + + packer_cmd = f'packer build {params_str} {args.template}' + cmd.logger.debug(packer_cmd) + exit_code, stdout, stderr = run_in_shell(packer_cmd, args.verbose) + if exit_code: + cmd.fail_shell('packer', exit_code, stdout, stderr) + + regex = re.compile(r'AMI: (?Pami-\w+)') + match = regex.search(stdout) + if not match: + cmd.fail('could not find AMI ID in packer output') + + images = cmd.cache.get('images', {}) + images[name] = match.group('id') + cmd.cache['images'] = images + finally: + command_delete_packer_security_group(cmd, args.deployment_name) + + cmd.logger.info('done.') + + +def command_create_stack( + cmd, deployment_name, stack_name, template_path, extra_params, wait_period=60): + cmd.logger.info(f'creating stack {stack_name}...') + + # doing this to be compatible with AWS CLI which specifies the template path as + # file://path/to/template. + url = urlparse(template_path) + cmd.logger.debug(f'template: {url.path}') + with open(url.path) as fh: + template = fh.read() + + vpc_id = command_get_config(cmd, 'vpc-id') + subnet_id = command_get_config(cmd, 'subnet-id') + admin_cidr = command_get_config(cmd, 'admin-cidr') + + params = [ + {'ParameterKey': 'DeploymentName', 'ParameterValue': deployment_name}, + {'ParameterKey': 'VpcId', 'ParameterValue': vpc_id}, + {'ParameterKey': 'SubnetId', 'ParameterValue': subnet_id}, + {'ParameterKey': 'AdminCidr', 'ParameterValue': admin_cidr}, + ] + params.extend(extra_params) + + cmd.logger.debug(f'using params {params}') + resp = cmd.aws.cf.create_stack( + StackName=stack_name, + TemplateBody=template, + Parameters=params, + OnFailure='DELETE', ) - if len(resp['Subnets']) == 1: - subnet_id = resp['Subnets'][0]['SubnetId'] - cmd.cache['subnet_id'] = subnet_id - return subnet_id - elif len(resp['Subnets']) > 1: - subnet_ids = ', '.join(subnet['SubnetId'] for subnet in resp['Subnets']) - cmd.fail(f'multiple subnets found: {subnet_ids}; use --subnet-id to specify ' - 'which one should be used') - else: - cmd.fail('no subnets found in the account') + cmd.logger.debug(f'stack ID: {resp["StackId"]}') + + cmd.logger.debug('waiting for the stack creation to complete...') + resp = cmd.aws.cf.describe_stacks(StackName=stack_name) + while resp['Stacks'][0]['StackStatus'] == 'CREATE_IN_PROGRESS': + time.sleep(wait_period) + resp = cmd.aws.cf.describe_stacks(StackName=stack_name) + + stack_status = resp['Stacks'][0]['StackStatus'] + if stack_status == 'CREATE_COMPLETE': + instances = cmd.cache.get('instances', {}) + cmd.logger.debug(f'getting info for {stack_name}...') + stack_instance_info = get_stack_instances_info(cmd.aws, stack_name) -def command_get_region(cmd, args, subnet_id=None): - region = cmd.cache.get('region') - if region: - return region + cmd.logger.info('instances:') + for name, instance in stack_instance_info.items(): + cmd.logger.info(f' {name}: {instance['dns_name']} ({instance['ip_address']})') + instances[name] = instance - if subnet_id is None: - subnet_id = command_get_subnet_id(cmd, args) + cmd.cache['instances'] = instances - resp = cmd.aws.ec2.describe_subnets(SubnetIds=[subnet_id]) - zone_id = resp['Subnets'][0]['AvailabilityZoneId'] + cmd.logger.info('done.') + else: # stack_status != 'CREATE_COMPLETE' + cmd.logger.error(f'creation failed: {stack_status}') + resp = cmd.aws.cf.describe_stack_events(StackName=stack_name) - resp = cmd.aws.ec2.describe_availability_zones(ZoneIds=[zone_id]) - region = resp['AvailabilityZones'][0]['RegionName'] + for event in resp['StackEvents']: + if event['ResourceStatus'] == 'CREATE_IN_PROGRESS': + break + status = event['ResourceStatus'] + reason = event.get("ResourceStatusReason", '') + cmd.logger.error(f'{status} {reason}') - cmd.cache['region'] = region - return region + cmd.fail(f'could not create stack {stack_name}') def command_connect(cmd, instance_name, user='ubuntu', should_log=True): @@ -306,22 +459,125 @@ def command_connect(cmd, instance_name, user='ubuntu', should_log=True): ) -def command_update_ca_cert_form_deb(cmd): - deb_path = cmd.cache.get('deb') - if not deb_path: - cmd.fail('deb not found') +def con_transfer_file(con, src, dest, owner=None, preserve_mode=True): + temp_dest = randomword(32) + os.path.basename(dest) + con.put(src, remote=temp_dest, preserve_mode=preserve_mode) + con.sudo(f'mv ~/{temp_dest} {dest}') + if owner: + con.sudo(f'chown -R {owner} {dest}') + + +def create_ec_key(key_path): + key = ec.generate_private_key(curve=ec.SECP256R1()) - with open(deb_path, 'rb') as fh: - deb = ar.Archive(fh) - data_buf = io.BytesIO(deb.open('data.tar.xz', 'rb').read()) - tf = tarfile.open(fileobj=data_buf, mode='r:xz') - cert_fh = tf.extractfile('./opt/veraison/certs/rootCA.crt') + with open(key_path, 'wb') as wfh: + wfh.write(key.private_bytes( + encoding=serialization.Encoding.PEM, + format=serialization.PrivateFormat.TraditionalOpenSSL, + encryption_algorithm=serialization.NoEncryption(), + )) - dest_path = os.path.join(cmd.cache.dir, 'ca-cert.crt') - with open(dest_path, 'wb') as wfh: - wfh.write(cert_fh.read()) + os.chmod(key_path, stat.S_IRUSR | stat.S_IWUSR) - cmd.cache['ca-cert'] = dest_path + return key + + +def create_ca_cert(cert_path, key_path): + key = create_ec_key(key_path) + + subject = issuer = x509.Name([ + x509.NameAttribute(NameOID.ORGANIZATION_NAME, 'Veraison'), + ]) + now = datetime.now(timezone.utc) + + cert = ( + x509.CertificateBuilder(public_key=key.public_key()) + .subject_name(subject) + .issuer_name(issuer) + .serial_number(x509.random_serial_number()) + .not_valid_before(now) + .not_valid_after(now + timedelta(days=3650)) + .add_extension( + x509.BasicConstraints(ca=True, path_length=None), + critical=True, + ) + .add_extension( + x509.KeyUsage( + digital_signature=True, + content_commitment=False, + key_encipherment=False, + data_encipherment=False, + key_agreement=False, + key_cert_sign=True, + crl_sign=True, + encipher_only=False, + decipher_only=False, + ), + critical=True, + ) + .add_extension( + x509.SubjectKeyIdentifier.from_public_key(key.public_key()), + critical=False, + ) + ).sign(key, hashes.SHA256()) + + with open(cert_path, 'wb') as wfh: + wfh.write(cert.public_bytes(serialization.Encoding.PEM)) + + return cert, key + + +def create_service_cert(service_name, service_host, dest_path, ca_cert, ca_key): + cert_path = os.path.join(dest_path, f'{service_name}.crt') + key_path = os.path.join(dest_path, f'{service_name}.key') + + key = create_ec_key(key_path) + + subject = x509.Name([ + x509.NameAttribute(NameOID.COMMON_NAME, service_host), + ]) + now = datetime.now(timezone.utc) + + cert = ( + x509.CertificateBuilder(public_key=key.public_key()) + .subject_name(subject) + .issuer_name(ca_cert.subject) + .serial_number(x509.random_serial_number()) + .not_valid_before(now) + .not_valid_after(now + timedelta(days=3650)) + .add_extension( + x509.SubjectAlternativeName([ + x509.DNSName(service_host), + x509.DNSName('localhost'), + ]), + critical=False, + ) + .add_extension( + x509.BasicConstraints(ca=False, path_length=None), + critical=True, + ) + .add_extension( + x509.ExtendedKeyUsage([ + x509.ExtendedKeyUsageOID.CLIENT_AUTH, # pyright: ignore + x509.ExtendedKeyUsageOID.SERVER_AUTH, # pyright: ignore + ]), + critical=False, + ).add_extension( + x509.SubjectKeyIdentifier.from_public_key(key.public_key()), + critical=False, + ) + .add_extension( + x509.AuthorityKeyIdentifier.from_issuer_subject_key_identifier( + ca_cert.extensions.get_extension_for_class(x509.SubjectKeyIdentifier).value, + ), + critical=False, + ) + ).sign(ca_key, hashes.SHA256()) + + with open(cert_path, 'wb') as wfh: + wfh.write(cert.public_bytes(serialization.Encoding.PEM)) + + return cert_path, key_path class DeploymentCache: @@ -330,12 +586,31 @@ class DeploymentCache: def dir(self): return os.path.dirname(self.path) + @property + def certs_dir(self): + return os.path.join(self.dir, 'certs') + + @property + def ca_cert_path(self): + return os.path.join(self.certs_dir, 'rootCA.crt') + + @property + def ca_key_path(self): + return os.path.join(self.certs_dir, 'rootCA.key') + + @property + def user_tag(self): + return self.db.get('user_tag', self.default_user_tag) + def __init__(self, name, cache_dir=None): + if not name: + raise ValueError('name cannot be empty') self.name = name if cache_dir is None: cache_dir = xdg.BaseDirectory.save_data_path('veraison/aws') self.path = os.path.join(cache_dir, f'{self.name}.db') self.db = SqliteDict(self.path) + self.default_user_tag = f'{socket.gethostname()}-{getpass.getuser()}' def get(self, key, default=None): return self.db.get(key, default) @@ -364,6 +639,24 @@ class StoreIntList(argparse.Action): setattr(namespace, self.dest, [int(v) for v in values.split(',')]) # pyright: ignore +class LogFormatter(logging.Formatter): + + fmt = f'{{}}%(asctime)s %(name)s %(levelname)s{COLOR_RESET}: %(message)s' + + level_formats = { + logging.DEBUG: fmt.format(COLOR_DARK_GREY), + logging.INFO: fmt.format(COLOR_MEDIUM_GREY), + logging.WARNING: fmt.format(COLOR_YELLOW), + logging.ERROR: fmt.format(COLOR_RED), + logging.CRITICAL: fmt.format(COLOR_BOLD_RED), + } + + def format(self, record): + log_fmt = self.level_formats.get(record.levelno) + formatter = logging.Formatter(log_fmt) + return formatter.format(record) + + class BaseCommand: name = None @@ -380,8 +673,12 @@ class BaseCommand: self.update_arguments(parser) def execute(self, args): + if args.verbose and args.quiet: + self.fail('only one of -v/--verbose or -q/--quiet may be specfied at a time') if args.verbose: self.logger.setLevel(logging.DEBUG) + elif args.quiet: + self.logger.setLevel(logging.WARNING) self.cache = DeploymentCache(args.deployment_name, args.cache_dir) try: @@ -396,11 +693,11 @@ class BaseCommand: def fail_shell(self, command, exit_code, stdout, stderr): stdout_file = f'/tmp/{args.deployment_name}-{command}-failure.stdout' with open(stdout_file, 'w') as wfh: - wfh.write(stdout) + wfh.write(stdout) stderr_file = f'/tmp/{args.deployment_name}-{command}-failure.stderr' with open(stderr_file, 'w') as wfh: - wfh.write(stderr) + wfh.write(stderr) self.fail(f'{command} failed with {exit_code}' f'\n\tSTDOUT is in {stdout_file}\n\tSTDERR is in {stderr_file}') @@ -412,84 +709,47 @@ class BaseCommand: raise NotImplementedError() -class CreateStackCommand(BaseCommand): +class CreateCombinedStackCommand(BaseCommand): - name = 'create-stack' - desc = 'create deployment\'s cloudformation stack' + name = 'create-combined-stack' + desc = 'create deployment\'s cloudformation stack running services on one instance' def update_arguments(self, parser): - parser.add_argument('-a', '--admin-cidr') - parser.add_argument( - '-t', '--template-body', - default=os.path.abspath(os.path.join( - os.path.dirname(__file__), - '../templates/stack-combined.yaml', - )), - ) parser.add_argument('-k', '--key-name') - parser.add_argument('-i', '--image-id') - parser.add_argument('-s', '--subnet-id') - parser.add_argument('-V', '--vpc-id') def run(self, args): - self.logger.info(f'creating stack {args.deployment_name}...') - - # doing this to be compatible with AWS CLI which specifies the template path as - # file://path/to/template. - url = urlparse(args.template_body) - self.logger.debug(f'template: {url.path}') - with open(url.path) as fh: - template = fh.read() - - params = [ - {'ParameterKey': 'DeploymentName', 'ParameterValue': args.deployment_name}, - {'ParameterKey': 'KeyName', 'ParameterValue': self._get_key_name(args)}, - {'ParameterKey': 'InstanceImage', 'ParameterValue': self._get_image(args)}, - {'ParameterKey': 'VpcId', 'ParameterValue': self._get_vpc_id(args)}, - {'ParameterKey': 'SubnetId', 'ParameterValue': self._get_subnet_id(args)}, - {'ParameterKey': 'AdminCidr', 'ParameterValue': self._get_cidr(args)}, + stack_name = f'{args.deployment_name}-combined' + template_path = os.path.abspath(os.path.join( + os.path.dirname(__file__), + '../templates/stack-combined.yaml', + )) + + key_name = self._get_key_name(args) + + combined_image = self.cache.get('images', {}).get('combined') + if not combined_image: + self.fail('could not find combined image in cache; ' + 'please run create-combined-image command') + + keycloak_image = self.cache.get('images', {}).get('keycloak') + if not keycloak_image: + self.fail('could not find keycloak image in cache; ' + 'please run create-keycloak-image command') + + self.logger.debug('looking up subnet CIDR...') + subnet_id = command_get_config(cmd, 'subnet-id') + resp = self.aws.ec2.describe_subnets(SubnetIds=[subnet_id]) + subnet_cidr = resp['Subnets'][0]['CidrBlock'] + + extra_params = [ + {'ParameterKey': 'KeyName', 'ParameterValue': key_name}, + {'ParameterKey': 'CombinedImage', 'ParameterValue': combined_image}, + {'ParameterKey': 'KeycloakImage', 'ParameterValue': keycloak_image}, + {'ParameterKey': 'SubnetCidr', 'ParameterValue': subnet_cidr}, ] - self.logger.debug(f'using params {params}') - resp = self.aws.cf.create_stack( - StackName=args.deployment_name, - TemplateBody=template, - Parameters=params, - OnFailure='DELETE', - ) - self.logger.debug(f'stack ID: {resp["StackId"]}') - - self.logger.debug('waiting for the stack creation to complete...') - resp = self.aws.cf.describe_stacks(StackName=args.deployment_name) - while resp['Stacks'][0]['StackStatus'] == 'CREATE_IN_PROGRESS': - time.sleep(args.wait_period) - resp = self.aws.cf.describe_stacks(StackName=args.deployment_name) - - stack_status = resp['Stacks'][0]['StackStatus'] - if stack_status == 'CREATE_COMPLETE': - self.logger.debug(f'getting info for {args.deployment_name}...') - deployment_info = get_deployment_info(self.aws, args.deployment_name) - instance = deployment_info['instance'] - self.logger.info(f'instance: {instance['dns_name']} ({instance['ip_address']})') - - self.logger.debug('updating cache') - instances = self.cache.get('instances', {}) - instances['combined'] = instance - self.cache['instances'] = instances - - self.logger.info('done.') - else: # stack_status != 'CREATE_COMPLETE' - self.logger.error(f'creation failed: {stack_status}') - resp = self.aws.cf.describe_stack_events(StackName=args.deployment_name) - - for event in resp['StackEvents']: - if event['ResourceStatus'] == 'CREATE_IN_PROGRESS': - break - status = event['ResourceStatus'] - reason = event.get("ResourceStatusReason", '') - self.logger.error(f'{status} {reason}') - - self.fail(f'could not create stack {args.deployment_name}') + command_create_stack(cmd, args.deployment_name, stack_name, + template_path, extra_params, args.wait_period) def _get_key_name(self, args): if args.key_name: @@ -502,86 +762,90 @@ class CreateStackCommand(BaseCommand): self.fail('could not find key name (specify with --key-name or run ' 'create-key-pair command)') - def _get_image(self, args): - if args.image_id: - return args.image_id - - image_id = self.cache.get('images', {}).get(f'{args.deployment_name}-combined') - if image_id: - return image_id - - self.fail('could not find IMA image ID (specify with --image-id or run ' - 'create-image command') - - def _get_cidr(self, args): - if args.admin_cidr: - return args.admin_cidr - - return f'{get_public_ip_address()}/32' - - def _get_vpc_id(self, args): - return command_get_vpc_id(self, args) - - def _get_subnet_id(self, args): - return command_get_subnet_id(self, args) - class DeleteStackCommand(BaseCommand): name = 'delete-stack' - desc = 'delete deployment\'s stack' + desc = 'delete a previously created stack' + + def update_arguments(self, parser): + parser.add_argument('name') def run(self, args): - self.logger.info(f'deleting stack {args.deployment_name}...') - self.aws.cf.delete_stack(StackName=args.deployment_name) + stack_name = f'{args.deployment_name}-{args.name}' + self.logger.info(f'deleting stack {stack_name}...') + self.logger.debug(f'getting {stack_name} instances...') + stack_instances = get_stack_instances_info(self.aws, stack_name) + + self.aws.cf.delete_stack(StackName=stack_name) try: self.logger.debug('waiting for the stack deletion to complete...') - resp = self.aws.cf.describe_stacks(StackName=args.deployment_name) + resp = self.aws.cf.describe_stacks(StackName=stack_name) while resp['Stacks'][0]['StackStatus'] == 'DELETE_IN_PROGRESS': time.sleep(args.wait_period) - resp = self.aws.cf.describe_stacks(StackName=args.deployment_name) + resp = self.aws.cf.describe_stacks(StackName=stack_name) except ClientError as e: if 'does not exist' not in str(e): raise e - self.logger.debug('updating cache') + self.logger.debug('updating instances in cache') instances = self.cache.get('instances', {}) - del instances['combined'] + for instance_name in stack_instances: + del instances[instance_name] self.cache['instances'] = instances self.logger.info('done.') -class UpdateSecurityGroups(BaseCommand): +class DeleteCertsCommand(BaseCommand): + + name = 'delete-certs' + desc = 'delete previously created service certs' + + def run(self, args): + self.logger.info('deleting service certificates...') + certs = self.cache.get('certs', {}) + if not certs: + self.logger.debug('no service certs found in cache') + + for cname, cpaths in certs.items(): + self.logger.debug(f'deleting {cname}...') + os.remove(cpaths['cert']) + os.remove(cpaths['key']) + + self.cache['certs'] = {} + self.logger.info('done.') + + +class UpdateSecurityGroupsCommand(BaseCommand): name = 'update-security-groups' desc = 'update security group(s) in deployment with current host\'s IP address' def update_arguments(self, parser): - default_tag = f'{socket.gethostname()}-{getpass.getuser()}' - parser.add_argument('-t', '--tag', default=default_tag) - parser.add_argument('-p', '--ports', action=StoreIntList, default=[8888, 8080, 8088, 22]) + parser.add_argument('-p', '--ports', action=StoreIntList, + default=[11111, 8888, 8080, 8088, 22]) def run(self, args): - self.logger.info('updating deployment security groups with IP address for this host') + self.logger.info('updating deployment security groups with IP address for this host...') try: - update_dynamic_address_rules(self.aws, args.deployment_name, args.tag, args.ports) + command_update_dynamic_address_rules( + self, args.deployment_name, self.cache.user_tag, args.ports) except Exception as e: self.fail(e) self.logger.info('done.') -class CreateImage(BaseCommand): +class CreateCombinedImageCommand(BaseCommand): - name = 'create-image' + name = 'create-combined-image' desc = 'create IMA image for the Veraison services EC2 instance' def update_arguments(self, parser): parser.add_argument('-D', '--deb') - parser.add_argument('-s', '--subnet-id') parser.add_argument( '-t', '--template', default=os.path.abspath(os.path.join( @@ -590,85 +854,224 @@ class CreateImage(BaseCommand): )), ) parser.add_argument('-T', '--instance-type') - parser.add_argument('-V', '--vpc-id') def run(self, args): - if not shutil.which('packer'): - self.fail('packer must be installed on the system') - - if not os.path.isfile(args.template): - self.fail(f'template {args.template} does not exist') - deb_path = args.deb or self.cache['deb'] if not os.path.isfile(deb_path): self.fail(f'{deb_path} does not exist') self.cache['deb'] = deb_path - name = f'{args.deployment_name}-combined' - self.logger.info(f'creating image: {name}...') + command_create_image(self, args, 'combined', {'deb': deb_path}) - self.logger.debug('checking for existing AMI with that name...') - existing_id = get_ami_id(self.aws, name) - if existing_id: - if not args.force: - self.fail(f'image {name} already exits (use -f to overwrite)') - self.logger.info('removing existing image...') - self.aws.ec2.deregister_image(ImageId=existing_id) - - self.logger.info('building using packer...') - subnet_id = command_get_subnet_id(self, args) - region = command_get_region(self, args, subnet_id) - packer_build_args = ' '.join(f'-var {k}={v}' for k, v in { - 'ami_name': name, - 'deb': deb_path, - 'deployment_name': args.deployment_name, - 'instance_type': args.instance_type, - 'region': region, - 'vpc_id': command_get_vpc_id(self, args), - 'subnet_id': subnet_id, - }.items() if v is not None) - packer_cmd = f'packer build {packer_build_args} {args.template}' - self.logger.debug(packer_cmd) - exit_code, stdout, stderr = run_in_shell(packer_cmd, args.verbose) - if exit_code: - self.fail_shell('packer', exit_code, stdout, stderr) +class ConfigureCommand(BaseCommand): - regex = re.compile(r'AMI: (?Pami-\w+)') - match = regex.search(stdout) - if not match: - self.fail('could not find AMI ID in packer output') + name = 'configure' + desc = 'configure deployment parameters' - images = self.cache.get('images', {}) - images[name] = match.group('id') - self.cache['images'] = images + default_provisioning_user = 'veraison-provisioner' + default_provisioning_password = 'veraison' + default_management_user = 'veraison-manager' + default_management_password = 'veraison' + default_client_id = 'veraison-client' + default_client_secret = 'YifmabB4cVSPPtFLAmHfq7wKaEHQn10Z' - self.logger.info('done.') + def update_arguments(self, parser): + parser.add_argument('-a', '--admin-cidr') + parser.add_argument('-i', '--init', action='store_true', + help='initialize config not explicitly specified') + parser.add_argument('-r', '--region') + parser.add_argument('-s', '--subnet-id') + parser.add_argument('-v', '--vpc-id') + + parser.add_argument('-p', '--provisioning-user' , default=None) + parser.add_argument('-P', '--provisioning-password' , default=None) + parser.add_argument('-m', '--management-user' , default=None) + parser.add_argument('-M', '--management-password' , default=None) + parser.add_argument('-C', '--client-id' , default=None) + parser.add_argument('-S', '--client-secret' , default=None) + + def run(self, args): + self._configure_vpc_id(args) + self._configure_subnet_id(args) + self._configure_region(args) + self._conifigure_admin_cidr(args) + self._conifigure_client_settings(args) + + def _conifigure_admin_cidr(self, args): + if args.admin_cidr: + self.cache['admin-cidr'] = args.admin_cidr + elif args.init: + self.logger.warning('setting admin CIDR to 0.0.0.0/0; this is not recommended: ' + 're-run with --admin-cidr option') + self.cache['admin-cidr'] = '0.0.0.0/0' + + def _conifigure_client_settings(self, args): + attrs = [ + 'provisioning_user', + 'provisioning_password', + 'management_user', + 'management_password', + 'client_id', + 'client_secret', + ] + + if args.init: + for attr in attrs: + default = getattr(self.__class__, f'default_{attr}') + setattr(args, attr, getattr(args, attr) or default) + + client_config = self.cache.get('client_config', {}) + + for attr in attrs: + new_val = getattr(args, attr) + if new_val: + client_config[attr] = new_val + + self.cache['client_config'] = client_config + + def _configure_vpc_id(self, args): + if args.vpc_id: + self.logger.debug(f'writing {args.vpc_id} to cache') + self.cache['vpc-id'] = args.vpc_id + return + elif not args.init and self.cache.get('vpc-id'): + return # already set and not re-initializing + + self.logger.debug('no VPC ID specified; trying to identify from account...') + resp = self.aws.ec2.describe_vpcs( + Filters=[{ + 'Name': 'state', + 'Values': ['available'], + }] + ) + if len(resp['Vpcs']) == 1: + vpc_id = resp['Vpcs'][0]['VpcId'] + self.cache['vpc-id'] = vpc_id + elif len(resp['Vpcs']) > 1: + vpc_ids = ', '.join(vpc['VpcId'] for vpc in resp['Vpcs']) + self.fail(f'multiple VPCs found: {vpc_ids}; use --vpc-id to specify ' + 'which one should be used') + else: + self.fail('no VPCs found in the account') + + def _configure_subnet_id(self, args): + if args.subnet_id: + self.logger.debug(f'writing {args.subnet_id} to cache') + self.cache['subnet-id'] = args.subnet_id + return + elif not args.init and self.cache.get('subnet-id'): + return # already set and not re-initializing + + self.logger.debug('no subnet ID specified; trying to identify from account...') + resp = self.aws.ec2.describe_subnets( + Filters=[{ + 'Name': 'state', + 'Values': ['available'], + }] + ) + if len(resp['Subnets']) == 1: + subnet_id = resp['Subnets'][0]['SubnetId'] + self.cache['subnet-id'] = subnet_id + elif len(resp['Subnets']) > 1: + subnet_ids = ', '.join(subnet['SubnetId'] for subnet in resp['Subnets']) + self.fail(f'multiple subnets found: {subnet_ids}; use --subnet-id to specify ' + 'which one should be used') + else: + self.fail('no subnets found in the account') + + def _configure_region(self, args): + if args.region: + self.logger.debug(f'writing {args.region} to cache') + self.cache['region'] = args.region + return + elif not args.init and self.cache.get('region'): + return # already set and not re-initializing + + subnet_id = command_get_config(self, 'subnet-id') + + resp = self.aws.ec2.describe_subnets(SubnetIds=[subnet_id]) + zone_id = resp['Subnets'][0]['AvailabilityZoneId'] + + resp = self.aws.ec2.describe_availability_zones(ZoneIds=[zone_id]) + region = resp['AvailabilityZones'][0]['RegionName'] + + self.cache['region'] = region -class DeleteImage(BaseCommand): +class CreateKeycloakImageCommand(BaseCommand): + + name = 'create-keycloak-image' + desc = 'create IMA image for the Keycloak EC2 instance' + + def update_arguments(self, parser): + parser.add_argument( + '-c', '--keycloak-config-template', + default=os.path.abspath(os.path.join( + os.path.dirname(__file__), + '../templates/keycloak.conf.template', + )), + ) + parser.add_argument( + '-s', '--keycloak-service-template', + default=os.path.abspath(os.path.join( + os.path.dirname(__file__), + '../templates/keycloak.service.template', + )), + ) + parser.add_argument( + '-t', '--template', + default=os.path.abspath(os.path.join( + os.path.dirname(__file__), + '../templates/image-keycloak.pkr.hcl', + )), + ) + parser.add_argument('-T', '--instance-type') + + def run(self, args): + if not os.environ.get('KEYCLOAK_ADMIN_PASSWORD'): + self.logger.debug('generating admin password for Keycloak and writing into cache...') + # note: not using punctuation in the initial password, as it will be passed through + # multiple shells, environments, and tools, and we don't want to + # warry about correctrly escaping everything at every stage. Using a longer string + # to compensate. + password = randomword(40) + os.environ['KEYCLOAK_ADMIN_PASSWORD'] = password + self.cache['kc_password'] = password + + conf_path = command_instantiate_template( + self, args.deployment_name, args.keycloak_config_template) + service_path = command_instantiate_template( + self, args.deployment_name, args.keycloak_service_template) + command_create_image(self, args, 'keycloak', + {'conf_path': conf_path, 'service_path': service_path}) + + +class DeleteImageCommand(BaseCommand): name = 'delete-image' desc = 'delete IMA image for the Veraison services EC2 instance' + def update_arguments(self, parser): + parser.add_argument('name') + def run(self, args): - name = f'{args.deployment_name}-combined' images = self.cache.get('images', {}) - iid = images.get(name) + iid = images.get(args.name) if iid is None: - self.fail(f'no entry for image {name} found in the deployment cache') + self.fail(f'no entry for image {args.name} found in the deployment cache') self.logger.info(f'deleting image {name} ({iid})...') self.aws.ec2.deregister_image(ImageId=iid) - self.logger.debug(f'removing image {name} from cache') - del images[name] + self.logger.debug(f'removing image {args.name} from cache') + del images[args.name] self.cache['images'] = images self.logger.info('done.') -class CreateKeyPair(BaseCommand): +class CreateKeyPairCommand(BaseCommand): name = 'create-key-pair' desc = 'create a key pair that will be used for SSH access to the deployment\'s instances' @@ -716,7 +1119,7 @@ class CreateKeyPair(BaseCommand): self.logger.info('done.') -class DeleteKeyPair(BaseCommand): +class DeleteKeyPairCommand(BaseCommand): name = 'delete-key-pair' desc = 'create a key pair that will be used for SSH access to the deployment\'s instances' @@ -770,7 +1173,7 @@ class DeleteKeyPair(BaseCommand): self.logger.info('done. (local files not touched)') -class CreateDeb(BaseCommand): +class CreateDebCommand(BaseCommand): name = 'create-deb' desc = 'create the Veraison Debian package' @@ -817,14 +1220,11 @@ class CreateDeb(BaseCommand): self.cache['deb'] = dest_path self.logger.info(f'created {dest_path}') - - self.logger.info('extracting ca-cert...') - command_update_ca_cert_form_deb(self) self.logger.info('done.') -class DeleteDeb(BaseCommand): +class DeleteDebCommand(BaseCommand): name = 'delete-deb' desc = 'delete perviously created Debian package' @@ -840,23 +1240,51 @@ class DeleteDeb(BaseCommand): os.remove(deb_path) del self.cache['deb'] - cert_path = self.cache.get('ca-cert') - if cert_path: - self.logger.debug(f'removing ca-cert {cert_path}') - os.remove(cert_path) - del self.cache['ca-cert'] - self.logger.info('done.') -class Shell(BaseCommand): +class CreateDebugStack(BaseCommand): + name = 'create-debug-stack' + desc = 'create a stack with a single instance that can be used for testing' + + def update_arguments(self, parser): + parser.add_argument('-k', '--key-name') + parser.add_argument('-i', '--image-id', default='ami-02c6977f57c0816de') + + def run(self, args): + stack_name = f'{args.deployment_name}-debug' + template_path = os.path.abspath(os.path.join( + os.path.dirname(__file__), + '../templates/stack-debug.yaml', + )) + key_name = self._get_key_name(args) + extra_params = [ + {'ParameterKey': 'KeyName', 'ParameterValue': key_name}, + {'ParameterKey': 'InstanceImage', 'ParameterValue': args.image_id}, + ] + + command_create_stack(cmd, args.deployment_name, stack_name, + template_path, extra_params, args.wait_period) + + def _get_key_name(self, args): + if args.key_name: + return args.key_name + + key_info = self.cache.get('key') + if key_info: + return key_info['name'] + + self.fail('could not find key name (specify with --key-name or run ' + 'create-key-pair command)') + +class ShellCommand(BaseCommand): name = 'shell' desc = 'start a shell on a deployment instance' def update_arguments(self, parser): + parser.add_argument('instance', choices=['combined', 'keycloak', 'test']) parser.add_argument('-k', '--ssh-key') - parser.add_argument('-i', '--instance' , default='combined', choices=['combined']) parser.add_argument('-u', '--user', default='ubuntu') parser.add_argument('-s', '--server-alive-interval', type=int, default=60) @@ -885,36 +1313,53 @@ class Shell(BaseCommand): os.system(ssh_cmd) -class UpdateCerts(BaseCommand): +class PushCommand(BaseCommand): - name = 'update-certs' - description = 'update SSL certs in the combine instance to reflect its DNS name' + name = 'push' + desc = 'copy a file/directory from the host to a deployment instance' + + def update_arguments(self, parser): + parser.add_argument('-i', '--instance' , default='combined', + choices=['combined', 'keycloak']) + parser.add_argument('-P', '--no-preserve-mode', action='store_true') + parser.add_argument('-s', '--sudo', action='store_true') + parser.add_argument('-o', '--owner', help='implies --sudo') + parser.add_argument('src') + parser.add_argument('dest') def run(self, args): - with command_connect(self, 'combined') as con: - self.logger.info(f'updating certs for {con.host}...') + if args.owner: + args.sudo = True + + with command_connect(self, args.instance) as con: + if args.sudo: + con_transfer_file( + args.src, args.dest, + args.owner, preserve_mode=(not args.no_preserve_mode)) + else: + con.put(args.src, remote=args.dest, preserve_mode=(not args.no_preserve_mode)) - cmd = ( - f'/opt/veraison/bin/veraison -f gen-service-certs {con.host},localhost ' - '/opt/veraison/certs/rootCA.{crt,key}' - ) - self.logger.debug(cmd) - # pty=True combines stdout and stderr - res = con.sudo(cmd, echo=False, hide=True, pty=True) - for line in res.stdout.split('\n'): - if line: - self.logger.debug(line) - - self.logger.debug('restating veraison services') - con.sudo('/opt/veraison/bin/veraison -s stop-services', - echo=False, hide=True, pty=True) - con.sudo('/opt/veraison/bin/veraison -s start-services', - echo=False, hide=True, pty=True) - self.logger.info('done.') +class PullCommand(BaseCommand): + + name = 'pull' + desc = 'copy a file/directory from a deployment instance to the host' + + def update_arguments(self, parser): + parser.add_argument('-i', '--instance' , default='combined', + choices=['combined', 'keycloak']) + parser.add_argument('-P', '--no-preserve-mode', action='store_true') + parser.add_argument('src') + parser.add_argument('dest') + + def run(self, args): + self.logger.debug(f'copying {args.src} on the host to {args.dest} ' + f'on {args.instance} instance') + with command_connect(self, args.instance) as con: + con.get(args.src, local=args.dest, preserve_mode=(not args.no_preserve_mode)) -class CreateClientConfig(BaseCommand): +class CreateClientConfigCommand(BaseCommand): name = 'create-client-config' desc = ''' @@ -923,16 +1368,21 @@ class CreateClientConfig(BaseCommand): all_clients = ['cocli', 'evcli', 'pocli'] def update_arguments(self, parser): - parser.add_argument('-c', '--client', action='append', choices=self.all_clients) + parser.add_argument('-c', '--client', + action='append', dest='clients', choices=self.all_clients) parser.add_argument('-o', '--output-dir', default=xdg.BaseDirectory.xdg_config_home) def run(self, args): self.logger.info('creating Veraison client config(s)...') - cert_path = self.cache.get('ca-cert') - if not cert_path: - self.fail('could not find ca-cert in cache') + if not os.path.isfile(self.cache.ca_cert_path): + self.fail('could not find ca-cert in cache; has create-certs been called?') + + client_config = self.cache.get('client_config') + if not client_config: + self.fail('client config not found; run configure command with appropriate options') with command_connect(self, 'combined') as con: + self.logger.debug(f'getting services config from {con.host}...') res = con.run( 'cat /opt/veraison/config/services/config.yaml', @@ -940,8 +1390,15 @@ class CreateClientConfig(BaseCommand): ) if res.exited != 0: self.fail(f'could not read services config; got {res.exited}: {res.stderr}') + srv_cfg = yaml.safe_load(res.stdout) + + kc_host = srv_cfg.get('auth', {}).get('host') + kc_port = srv_cfg.get('auth', {}).get('port') + if not (kc_host and kc_port): + self.fail('keycloak host/port not found in services config; ' + 'has auth been configured?') - clients = args.client or self.all_clients + clients = args.clients or self.all_clients for client in clients: self.logger.info(f'generating {client} config...') outdir = os.path.join(args.output_dir, client) @@ -949,52 +1406,95 @@ class CreateClientConfig(BaseCommand): self.logger.debug(f'creating {outdir}') os.makedirs(outdir) - srv_cfg = yaml.safe_load(res.stdout) - config = getattr(self, f'get_{client}_config')(srv_cfg, con.host, cert_path) + config = getattr(self, f'get_{client}_config')( + srv_cfg, client_config, con.host, kc_host, kc_port, + ) outfile = os.path.join(outdir, 'config.yaml') self.logger.debug(f'writing {outfile}') with open(outfile, 'w') as wfh: yaml.dump(config, wfh) - + self.cache['client_config_dir'] = args.output_dir self.logger.info('done.') - def get_cocli_config(self, srv_cfg, host, cert_path): + def get_cocli_config(self, srv_cfg, cli_cfg, host, kc_host, kc_port): port = int(srv_cfg['provisioning']['listen-addr'].split(':')[1]) return { - 'ca_cert': cert_path, + 'ca_cert': self.cache.ca_cert_path, 'api_server': f'https://{host}:{port}/endorsement-provisioning/v1/submit', + 'auth': 'oauth2', + 'username': cli_cfg['provisioning_user'], + 'password': cli_cfg['provisioning_password'], + 'client_id': cli_cfg['client_id'], + 'client_secret': cli_cfg['client_secret'], + 'token_url': f'https://{kc_host}:{kc_port}' + '/realms/veraison/protocol/openid-connect/token', } - def get_evcli_config(self, srv_cfg, host, cert_path): + def get_evcli_config(self, srv_cfg, cli_cfg, host, kc_host, kc_port): port = int(srv_cfg['verification']['listen-addr'].split(':')[1]) return { - 'ca_cert': cert_path, + 'ca_cert': self.cache.ca_cert_path, 'api_server': f'https://{host}:{port}/challenge-response/v1/newSession', } - def get_pocli_config(self, srv_cfg, host, cert_path): + def get_pocli_config(self, srv_cfg, cli_cfg, host, kc_host, kc_port): port = int(srv_cfg['management']['listen-addr'].split(':')[1]) return { - 'ca_cert': cert_path, + 'ca_cert': self.cache.ca_cert_path, 'tls': True, 'host': host, 'port': port, + 'auth': 'oauth2', + 'username': cli_cfg['management_user'], + 'password': cli_cfg['management_password'], + 'client_id': cli_cfg['client_id'], + 'client_secret': cli_cfg['client_secret'], + 'token_url': f'https://{kc_host}:{kc_port}' + '/realms/veraison/protocol/openid-connect/token', } -class Cache(BaseCommand): +class CacheCommand(BaseCommand): name = 'cache' desc = 'show cached info for the deployment' + def update_arguments(self, parser): + parser.add_argument('-q', '--query') + def run(self, args): - print(f'deployment: {args.deployment_name}') - pprint.pp(self.cache.as_dict()) + if args.query: + parts = args.query.split('.') + entry = self.cache + path = '' + + for part in parts[:len(parts)-1]: + entry = self._access_member(entry, part, path) + path = path + '.' + part + + val = self._access_member(entry, parts.pop(), path) + if val is not None: + sys.stdout.write(val) + else: + print(f'deployment: {args.deployment_name}') + pprint.pp(self.cache.as_dict()) + + def _access_member(self, entry, part, path): + try: # if part is an int, assume list entry + idx = int(part) + if (len(entry)-1) > idx: # pyright: ignore[reportArgumentType] + if path: + self.fail(f'index {idx} does not exist for cache entry "{path}"') + else: + self.fail(f'index {idx} does not exist in cache') + return entry[idx] + except ValueError: # part not an int, assume dict entry + return entry[part] -class CheckStores(BaseCommand): +class CheckStoresCommand(BaseCommand): name = 'check-stores' desc = 'output the contents of deployment\'s sqlite3 stores' aliases = ['stores'] @@ -1008,16 +1508,16 @@ class CheckStores(BaseCommand): print(res.stdout.rstrip('\n')) -class Status(BaseCommand): +class StatusCommand(BaseCommand): name = 'status' desc = 'show status of the deployment' def run(self, args): print(f'deployment: {args.deployment_name}') - vpc_id = self.cache.get('vpc_id', f'{COLOR_DARK_GREY}none{COLOR_RESET}') + vpc_id = self.cache.get('vpc-id', f'{COLOR_DARK_GREY}none{COLOR_RESET}') print(f' vpc: {vpc_id}') - subnet_id = self.cache.get('subnet_id', f'{COLOR_DARK_GREY}none{COLOR_RESET}') + subnet_id = self.cache.get('subnet-id', f'{COLOR_DARK_GREY}none{COLOR_RESET}') print(f' subnet: {subnet_id}') instance = self.cache.get('instances', {}).get('combined') @@ -1039,7 +1539,7 @@ class Status(BaseCommand): print(f' instance: {host} ({addr}) {COLOR_RED}down{COLOR_RESET}') -class ClearStores(BaseCommand): +class ClearStoresCommand(BaseCommand): name = 'clear-stores' desc = 'clear the contents of deployment\'s sqlite3 stores' @@ -1051,22 +1551,185 @@ class ClearStores(BaseCommand): self.fail(f'could not clear stores; got {res.exited}: {res.stdout}') -class LogFormatter(logging.Formatter): +class CreateCertsCommand(BaseCommand): - fmt = f'{{}}%(asctime)s %(name)s %(levelname)s{COLOR_RESET}: %(message)s' + name = 'create-certs' + desc = 'generate certificates for the deployment' - level_formats = { - logging.DEBUG: fmt.format(COLOR_DARK_GREY), - logging.INFO: fmt.format(COLOR_GREY), - logging.WARNING: fmt.format(COLOR_YELLOW), - logging.ERROR: fmt.format(COLOR_RED), - logging.CRITICAL: fmt.format(COLOR_BOLD_RED), - } + all_services = ['vts', 'provisioning', 'verification', 'management', 'keycloak'] - def format(self, record): - log_fmt = self.level_formats.get(record.levelno) - formatter = logging.Formatter(log_fmt) - return formatter.format(record) + def update_arguments(self, parser): + parser.add_argument('-c', '--ca-cert') + parser.add_argument('-k', '--ca-cert-key') + parser.add_argument('-s', '--service', + action='append', dest='services', choices=self.all_services) + + def run(self, args): + self.logger.info('creating service certificates...') + if (not args.ca_cert) != (not args.ca_cert_key): + self.fail('if one of -c/--ca-cert and -k/--ca-cert-key is specified, ' + 'the other must be as well') + + combined_host = self.cache.get('instances', {}).get('combined', {}).get('dns_name') + if not combined_host: + self.fail('did not find DNS name for combined instance in cache; ' + 'has create-stack been called?') + + keycloak_host = self.cache.get('instances', {}).get('keycloak', {}).get('dns_name') + if not keycloak_host: + self.fail('did not find DNS name for keycloak instance in cache; ' + 'has create-stack been called?') + + if not os.path.isdir(self.cache.certs_dir): + self.logger.debug(f'creating {self.cache.certs_dir}') + os.makedirs(self.cache.certs_dir) + + if args.ca_cert: + self.logger.debug('copying CA cert to cache') + shutil.copyfile(args.ca_cert, self.cache.ca_cert_path) + shutil.copyfile(args.ca_cert_key, self.cache.ca_key_path) + os.chmod(self.cache.ca_key_path, stat.S_IRUSR | stat.S_IWUSR) + + with open(self.cache.ca_key_path, 'rb') as fh: + ca_key = serialization.load_pem_private_key(fh.read(), None) + with open(self.cache.ca_cert_path, 'rb') as fh: + ca_cert = x509.load_pem_x509_certificate(fh.read()) + else: + if os.path.isfile(self.cache.ca_cert_path) and not args.force: + self.logger.debug('using existing CA cert') + with open(self.cache.ca_key_path, 'rb') as fh: + ca_key = serialization.load_pem_private_key(fh.read(), None) + with open(self.cache.ca_cert_path, 'rb') as fh: + ca_cert = x509.load_pem_x509_certificate(fh.read()) + else: + self.logger.debug('creating CA cert') + ca_cert, ca_key = create_ca_cert(self.cache.ca_cert_path, self.cache.ca_key_path) + + cache_entries = self.cache.get('certs', {}) + services = args.services or self.all_services + for service in services: + self.logger.debug(f'creating cert for {service}') + service_host = keycloak_host if service == 'keycloak' else combined_host + + cert_path = os.path.join(self.cache.certs_dir, f'{service}.crt') + if os.path.isfile(cert_path) and not args.force: + self.fail(f'cert for {service} already exists; use --force to overwrite') + + cert_path, key_path = create_service_cert( + service, service_host, self.cache.certs_dir, ca_cert, ca_key, + ) + cache_entries[service] = {'cert': cert_path, 'key': key_path} + + self.cache['certs'] = cache_entries + self.logger.info('done.') + + +class SetupServicesCommand(BaseCommand): + + name = 'setup-services' + desc = 'set up the services instance, updating certs and configuration' + + def update_arguments(self, parser): + parser.add_argument( + '-c', '--services-config-template', + default=os.path.abspath(os.path.join( + os.path.dirname(__file__), + '../templates/combined-services-config.yaml.template', + )), + ) + + def run(self, args): + self.logger.info('setting up services on the combined instance...') + + certs = self.cache.get('certs') + if not certs: + self.fail('certificates have not been created; run create-certificates command') + + kc_host = self.cache.get('instances', {}).get('keycloak', {}).get('dns_name') + if not kc_host: + self.fail('could not find Keycloak host; run create-combined-stack command') + os.environ['KEYCLOAK_HOST'] = kc_host + + config_path = command_instantiate_template( + self, args.deployment_name, args.services_config_template) + + self.logger.debug('connecting to combined instance...') + with command_connect(self, 'combined') as con: + self.logger.debug('tranfering updated config...') + con_transfer_file(con, + config_path, f'/opt/veraison/config/services/config.yaml', + owner='veraison:veraison') + + self.logger.debug('tranfering CA cert...') + con_transfer_file(con, + self.cache.ca_cert_path, '/opt/veraison/certs/rootCA.crt', + owner='veraison:veraison') + # delete any previous root key as it will no longer match the + # provision rootCA.crt (and is not needed on the services node). + con.sudo('rm -f /opt/veraison/certs/rootCA.key') + + for cname, cpaths in certs.items(): + if cname == 'keycloak': + continue + self.logger.debug(f'transfering {cname} cert and key...') + con_transfer_file(con, + cpaths['cert'], f'/opt/veraison/certs/{cname}.crt', + owner='veraison:veraison') + con_transfer_file(con, + cpaths['key'], f'/opt/veraison/certs/{cname}.key', + owner='veraison:veraison') + + self.logger.debug('restating veraison services...') + con.sudo('/opt/veraison/bin/veraison -s stop-services', + echo=False, hide=True, pty=True) + con.sudo('/opt/veraison/bin/veraison -s start-services', + echo=False, hide=True, pty=True) + + self.logger.info('done.') + + +class SetupKeycloakCommand(BaseCommand): + + name = 'setup-keycloak' + desc = 'set up the keycloak instance, updating certs and starting the service' + + def update_arguments(self, parser): + parser.add_argument('-r', '--realm-file') + + def run(self, args): + self.logger.info('setting up keycloak instance...') + + cpaths = self.cache.get('certs', {}).get('keycloak') + if not cpaths: + self.fail('certificates have not been created; run create-certificates command') + + self.logger.debug('connecting to keycloak instance...') + with command_connect(self, 'keycloak') as con: + self.logger.debug('transfering keycloak cert and key...') + con_transfer_file(con, + cpaths['cert'], '/opt/keycloak/certs/keycloak.crt', + owner='keycloak:keycloak') + con_transfer_file(con, + cpaths['key'], '/opt/keycloak/certs/keycloak.key', + owner='keycloak:keycloak') + + if args.realm_file: + self.logger.debug(f'transfering keycloak {args.realm_file}...') + filename = os.path.basename(args.realm_file) + con_transfer_file(con, + args.realm_file, f'/opt/keycloak/data/import/{filename}', + owner='keycloak:keycloak') + + self.logger.debug('stopping keycloak...') + con.sudo('systemctl stop keycloak') + + self.logger.debug('rebuilding keycloak...') + con.sudo('/opt/keycloak/bin/kc.sh build', hide=True) + + self.logger.debug('starting keycloak...') + con.sudo('systemctl start keycloak') + + self.logger.info('done.') if __name__ == '__main__': @@ -1099,12 +1762,21 @@ if __name__ == '__main__': cmd_map[alias] = cmd parser = argparse.ArgumentParser() - parser.add_argument('-d', '--deployment-name', default='veraison-deployment') - parser.add_argument('-f', '--force', action='store_true') - parser.add_argument('-W', '--wait-period', type=int, default=1) - parser.add_argument('-v', '--verbose', action='store_true') + parser.add_argument('-d', '--deployment-name', + help='the name for this deployment; this is used in a number ' + 'places, including AWS resources tags') + parser.add_argument('-f', '--force', action='store_true', + help='force overwrite of exiting resources') + parser.add_argument('-W', '--wait-period', type=int, default=1, + help='period (in seconds) to wait between polls to AWS for ' + 'long-running command progress') + parser.add_argument('-v', '--verbose', action='store_true', + help='show DEBUG level messages') + parser.add_argument('-q', '--quiet', action='store_true', + help='hide INFO level messages') parser.add_argument( '--cache-dir', default=xdg.BaseDirectory.save_data_path('veraison/aws'), + help='location that will be used for local deployment data', ) subparsers = parser.add_subparsers(dest='command', required=True) @@ -1113,6 +1785,21 @@ if __name__ == '__main__': command.register(subparsers) args = parser.parse_args() + + os.makedirs(args.cache_dir, exist_ok=True) + current_deployment_path = os.path.join(args.cache_dir, 'current_deployment') + if args.deployment_name: + with open(current_deployment_path, 'w') as wfh: + wfh.write(args.deployment_name) + else: + if os.path.isfile(current_deployment_path): + with open(current_deployment_path) as fh: + args.deployment_name = fh.read().strip() + else: + logging.critical('no current deployment exists; ' + 'please use -d/--deployment-name to specify') + sys.exit(1) + cmd = cmd_map[args.command] try: cmd.execute(args) diff --git a/deployments/aws/deployment.cfg b/deployments/aws/deployment.cfg index 5a5dd316..a8ef65a6 100644 --- a/deployments/aws/deployment.cfg +++ b/deployments/aws/deployment.cfg @@ -17,10 +17,12 @@ VTS_PORT=${VTS_PORT:-50051} PROVISIONING_PORT=${PROVISIONING_PORT:-8888} VERIFICATION_PORT=${VERIFICATION_PORT:-8080} MANAGEMENT_PORT=${MANAGEMENT_PORT:-8088} +KEYCLOAK_PORT=${KEYCLOAK_PORT:-11111} # The location of the Python venv that will be used to run the deployment # script. This venv must have appropriate dependencies installed (see -# misc/requirements.txt). +# misc/requirements.txt). The environment may be initialized using the +# bootstrap command of the deployment.sh script. VERAISON_AWS_VENV=${VERAISON_AWS_VENV:-~/venv/aws} # The name of the deployment. This will be used to name the CloudFormation @@ -28,6 +30,11 @@ VERAISON_AWS_VENV=${VERAISON_AWS_VENV:-~/venv/aws} # "veraison-deployment" and this value. VERAISON_AWS_DEPLOYMENT=${VERAISON_AWS_DEPLOYMENT:-veraison-deployment} +# The name of the AWS region into which Veraison will be deployed. Must be a valid +# AWS region name, see: +# https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/using-regions-availability-zones.html#concepts-available-regions +VERAISON_AWS_REGION=${VERAISON_AWS_REGION:-eu-west-1} + # ID of the VPC into which the deployment will be created. This must exist in # the account. VERAISON_AWS_VPC_ID=${VERAISON_AWS_VPC_ID:-} @@ -39,3 +46,9 @@ VERAISON_AWS_SUBNET_ID=${VERAISON_AWS_SUBNET_ID:-} # Instances' security groups will be configures to allow connections from this # CIDR. VERAISON_AWS_ADMIN_CIDR=${VERAISON_AWS_ADMIN_CIDR:-217.140.96.0/20} + +# Keycloak initial admin account credentials. If the password is not specified, it +# will be randomly generated during image creation, and will be written into the +# deployment cache. +KEYCLOAK_ADMIN=${KEYCLOAK_ADMIN:-admin} +KEYCLOAK_ADMIN_PASSWORD=${KEYCLOAK_ADMIN_PASSWORD:-} diff --git a/deployments/aws/deployment.sh b/deployments/aws/deployment.sh index 16021087..805b5650 100755 --- a/deployments/aws/deployment.sh +++ b/deployments/aws/deployment.sh @@ -51,12 +51,14 @@ function bootstrap() { cut -f2 -d= | tr -d \") case $distrib_id in - Arch) sudo pacman -Syy packer ssh;; + Arch) sudo pacman -Syy packer ssh openssl;; Ubuntu) - sudo apt --yes install curl + sudo apt update + sudo apt --yes install curl openssl + curl -fsSL https://apt.releases.hashicorp.com/gpg | sudo apt-key add - sudo apt-add-repository "deb [arch=amd64] https://apt.releases.hashicorp.com $(lsb_release -cs) main" - sudo apt update && sudo apt --yes install packer + sudo apt --yes install packer ;; *) echo -e "$_error: Boostrapping is currently only supported for Arch and Ubuntu." @@ -96,68 +98,48 @@ function bootstrap() { echo "$message" } -function create_image() { +function bringup() { + _check_installed openssl _check_installed packer - veraison create-image --vpc-id "${VERAISON_AWS_VPC_ID}" \ - --subnet-id "${VERAISON_AWS_SUBNET_ID}" -} - -function delete_image() { - veraison delete-image -} - -function create_key_pair() { - veraison create-key-pair -} - -function delete_key_pair() { - veraison delete-key-pair -} - -function create_deb() { - veraison create-deb -} - -function cache() { - veraison cache -} - -function create_stack() { - veraison create-stack --vpc-id "${VERAISON_AWS_VPC_ID}" \ + veraison configure --init \ + --vpc-id "${VERAISON_AWS_VPC_ID}" \ --subnet-id "${VERAISON_AWS_SUBNET_ID}" \ - --admin-cidr "${VERAISON_AWS_ADMIN_CIDR}" -} + --admin-cidr "${VERAISON_AWS_ADMIN_CIDR}" \ + --region "${VERAISON_AWS_REGION}" -function delete_stack() { - veraison delete-stack -} + veraison create-deb + veraison create-key-pair + veraison create-combined-image + veraison create-keycloak-image + veraison create-combined-stack -function update_security_groups() { veraison update-security-groups + veraison create-certs --ca-cert "${VERAISON_CA_CERT}" \ + --ca-cert-key "${VERAISON_CA_CERT_KEY}" + veraison setup-keycloak --realm-file "${_this_dir}/misc/veraison-realm.json" + veraison setup-services } -function update_certs() { - veraison update-certs -} +function redeploy_stack() { + _check_installed openssl -function bringup() { - _check_installed packer + veraison delete-stack combined + veraison delete-certs - veraison create-deb - veraison create-key-pair - veraison create-image --vpc-id "${VERAISON_AWS_VPC_ID}" \ - --subnet-id "${VERAISON_AWS_SUBNET_ID}" - veraison create-stack --vpc-id "${VERAISON_AWS_VPC_ID}" \ - --subnet-id "${VERAISON_AWS_SUBNET_ID}" \ - --admin-cidr "${VERAISON_AWS_ADMIN_CIDR}" + veraison create-combined-stack veraison update-security-groups - veraison update-certs + veraison create-certs --ca-cert "${VERAISON_CA_CERT}" \ + --ca-cert-key "${VERAISON_CA_CERT_KEY}" + veraison setup-keycloak --realm-file "${_this_dir}/misc/veraison-realm.json" + veraison setup-services } function teardown() { - veraison delete-stack - veraison delete-image + veraison delete-stack combined + veraison delete-certs + veraison delete-image keycloak + veraison delete-image combined veraison delete-key-pair veraison delete-deb } @@ -206,17 +188,8 @@ case $_command in help) help;; bootstrap) bootstrap;; bringup) bringup;; + redeploy-stack) redeploy_stack;; teardown) teardown;; - create-image) create_image;; - delete-image) delete_image;; - create-key-pair | create-key) create_key_pair;; - delete-key-pair | delete-key) delete_key_pair;; - create-stack) create_stack;; - delete-stack) delete_stack;; - create-deb) create_deb;; - update-security-groups) update_security_groups;; - update-certs) update_certs;; - cache) cache;; *) echo -e "$_error: unexpected command: \"$_command\"";; esac # vim: set noet sts=8 sw=8: diff --git a/deployments/aws/misc/requirements.txt b/deployments/aws/misc/requirements.txt index 9032808c..0819375b 100644 --- a/deployments/aws/misc/requirements.txt +++ b/deployments/aws/misc/requirements.txt @@ -1,6 +1,7 @@ -ar==1.0.0 boto3==1.35.8 botocore==1.35.8 +cryptography==43.0.1 +envsubst==0.1.5 fabric==3.2.2 pyxdg==0.28 PyYAML==6.0.2 diff --git a/deployments/aws/misc/veraison-realm.json b/deployments/aws/misc/veraison-realm.json new file mode 100644 index 00000000..0fd76cb4 --- /dev/null +++ b/deployments/aws/misc/veraison-realm.json @@ -0,0 +1,1841 @@ +{ + "id" : "f1c336ca-84a0-4bbf-b075-d6276bfb8f5e", + "realm" : "veraison", + "notBefore" : 0, + "defaultSignatureAlgorithm" : "RS256", + "revokeRefreshToken" : false, + "refreshTokenMaxReuse" : 0, + "accessTokenLifespan" : 1800, + "accessTokenLifespanForImplicitFlow" : 1800, + "ssoSessionIdleTimeout" : 1800, + "ssoSessionMaxLifespan" : 36000, + "ssoSessionIdleTimeoutRememberMe" : 0, + "ssoSessionMaxLifespanRememberMe" : 0, + "offlineSessionIdleTimeout" : 2592000, + "offlineSessionMaxLifespanEnabled" : false, + "offlineSessionMaxLifespan" : 5184000, + "clientSessionIdleTimeout" : 0, + "clientSessionMaxLifespan" : 0, + "clientOfflineSessionIdleTimeout" : 0, + "clientOfflineSessionMaxLifespan" : 0, + "accessCodeLifespan" : 60, + "accessCodeLifespanUserAction" : 300, + "accessCodeLifespanLogin" : 1800, + "actionTokenGeneratedByAdminLifespan" : 43200, + "actionTokenGeneratedByUserLifespan" : 300, + "oauth2DeviceCodeLifespan" : 600, + "oauth2DevicePollingInterval" : 5, + "enabled" : true, + "sslRequired" : "external", + "registrationAllowed" : false, + "registrationEmailAsUsername" : false, + "rememberMe" : false, + "verifyEmail" : false, + "loginWithEmailAllowed" : true, + "duplicateEmailsAllowed" : false, + "resetPasswordAllowed" : false, + "editUsernameAllowed" : false, + "bruteForceProtected" : false, + "permanentLockout" : false, + "maxFailureWaitSeconds" : 900, + "minimumQuickLoginWaitSeconds" : 60, + "waitIncrementSeconds" : 60, + "quickLoginCheckMilliSeconds" : 1000, + "maxDeltaTimeSeconds" : 43200, + "failureFactor" : 30, + "roles" : { + "realm" : [ { + "id" : "3c14c82e-ef6e-4c35-8fc5-b95322e12698", + "name" : "manager", + "description" : "Manages policies.", + "composite" : false, + "clientRole" : false, + "containerId" : "f1c336ca-84a0-4bbf-b075-d6276bfb8f5e", + "attributes" : { } + }, { + "id" : "29a6925f-81e8-4177-bb95-3c36bf4b7645", + "name" : "offline_access", + "description" : "${role_offline-access}", + "composite" : false, + "clientRole" : false, + "containerId" : "f1c336ca-84a0-4bbf-b075-d6276bfb8f5e", + "attributes" : { } + }, { + "id" : "03e833d2-8f65-4e1d-848a-87a01650b4d5", + "name" : "provisioner", + "description" : "Provisions trust anchors and endorsements.", + "composite" : false, + "clientRole" : false, + "containerId" : "f1c336ca-84a0-4bbf-b075-d6276bfb8f5e", + "attributes" : { } + }, { + "id" : "3c85b41b-2cd1-40af-9e31-c6a9d24114ce", + "name" : "default-roles-veraison", + "description" : "${role_default-roles}", + "composite" : true, + "composites" : { + "realm" : [ "offline_access", "uma_authorization" ], + "client" : { + "account" : [ "view-profile", "manage-account" ] + } + }, + "clientRole" : false, + "containerId" : "f1c336ca-84a0-4bbf-b075-d6276bfb8f5e", + "attributes" : { } + }, { + "id" : "a40e1662-6ddb-4fd7-829a-ffece56b48d2", + "name" : "uma_authorization", + "description" : "${role_uma_authorization}", + "composite" : false, + "clientRole" : false, + "containerId" : "f1c336ca-84a0-4bbf-b075-d6276bfb8f5e", + "attributes" : { } + } ], + "client" : { + "realm-management" : [ { + "id" : "66fc745f-b46a-4efa-bd17-77e060d7b2a0", + "name" : "view-realm", + "description" : "${role_view-realm}", + "composite" : false, + "clientRole" : true, + "containerId" : "4882f437-a423-46a1-878c-10616b7d6117", + "attributes" : { } + }, { + "id" : "b0d0efb7-3691-4ead-b33a-f16191bc5789", + "name" : "realm-admin", + "description" : "${role_realm-admin}", + "composite" : true, + "composites" : { + "client" : { + "realm-management" : [ "view-realm", "view-authorization", "view-clients", "manage-identity-providers", "query-clients", "query-groups", "manage-events", "create-client", "manage-users", "manage-clients", "view-users", "view-identity-providers", "manage-realm", "manage-authorization", "query-realms", "impersonation", "query-users", "view-events" ] + } + }, + "clientRole" : true, + "containerId" : "4882f437-a423-46a1-878c-10616b7d6117", + "attributes" : { } + }, { + "id" : "a34cb720-d7c5-408b-8b52-ed426d6d809c", + "name" : "view-authorization", + "description" : "${role_view-authorization}", + "composite" : false, + "clientRole" : true, + "containerId" : "4882f437-a423-46a1-878c-10616b7d6117", + "attributes" : { } + }, { + "id" : "44017973-fe85-45b5-b7f5-f53a757bce73", + "name" : "view-clients", + "description" : "${role_view-clients}", + "composite" : true, + "composites" : { + "client" : { + "realm-management" : [ "query-clients" ] + } + }, + "clientRole" : true, + "containerId" : "4882f437-a423-46a1-878c-10616b7d6117", + "attributes" : { } + }, { + "id" : "00eb870e-9ca7-4933-8809-4c536c1a22f6", + "name" : "manage-identity-providers", + "description" : "${role_manage-identity-providers}", + "composite" : false, + "clientRole" : true, + "containerId" : "4882f437-a423-46a1-878c-10616b7d6117", + "attributes" : { } + }, { + "id" : "c1fa45d6-dd02-4487-9cc2-6293f825467b", + "name" : "query-clients", + "description" : "${role_query-clients}", + "composite" : false, + "clientRole" : true, + "containerId" : "4882f437-a423-46a1-878c-10616b7d6117", + "attributes" : { } + }, { + "id" : "0d5c3ba5-7763-472c-9239-f946931e413f", + "name" : "query-groups", + "description" : "${role_query-groups}", + "composite" : false, + "clientRole" : true, + "containerId" : "4882f437-a423-46a1-878c-10616b7d6117", + "attributes" : { } + }, { + "id" : "fcc850bb-f082-4dd6-b5ab-510d1cc89311", + "name" : "create-client", + "description" : "${role_create-client}", + "composite" : false, + "clientRole" : true, + "containerId" : "4882f437-a423-46a1-878c-10616b7d6117", + "attributes" : { } + }, { + "id" : "2b8e4b72-1c63-4528-9be5-271868c1372a", + "name" : "manage-events", + "description" : "${role_manage-events}", + "composite" : false, + "clientRole" : true, + "containerId" : "4882f437-a423-46a1-878c-10616b7d6117", + "attributes" : { } + }, { + "id" : "bcf9a96e-d875-4548-adb0-8768404e54f7", + "name" : "manage-users", + "description" : "${role_manage-users}", + "composite" : false, + "clientRole" : true, + "containerId" : "4882f437-a423-46a1-878c-10616b7d6117", + "attributes" : { } + }, { + "id" : "a9cb8f89-fec3-4679-a0fc-3557cacc8c0f", + "name" : "manage-clients", + "description" : "${role_manage-clients}", + "composite" : false, + "clientRole" : true, + "containerId" : "4882f437-a423-46a1-878c-10616b7d6117", + "attributes" : { } + }, { + "id" : "9186b791-8864-4373-85d4-f27a4895ec9b", + "name" : "view-users", + "description" : "${role_view-users}", + "composite" : true, + "composites" : { + "client" : { + "realm-management" : [ "query-groups", "query-users" ] + } + }, + "clientRole" : true, + "containerId" : "4882f437-a423-46a1-878c-10616b7d6117", + "attributes" : { } + }, { + "id" : "67eeb9b5-6368-4489-9606-d04a7c78e330", + "name" : "view-identity-providers", + "description" : "${role_view-identity-providers}", + "composite" : false, + "clientRole" : true, + "containerId" : "4882f437-a423-46a1-878c-10616b7d6117", + "attributes" : { } + }, { + "id" : "50da5487-88c2-4c51-b95c-a7249e283df2", + "name" : "manage-realm", + "description" : "${role_manage-realm}", + "composite" : false, + "clientRole" : true, + "containerId" : "4882f437-a423-46a1-878c-10616b7d6117", + "attributes" : { } + }, { + "id" : "aca9a2d7-f1bc-4b2e-827d-d366fdfce45a", + "name" : "manage-authorization", + "description" : "${role_manage-authorization}", + "composite" : false, + "clientRole" : true, + "containerId" : "4882f437-a423-46a1-878c-10616b7d6117", + "attributes" : { } + }, { + "id" : "93cdc235-c527-4818-9601-3f27503a5988", + "name" : "query-realms", + "description" : "${role_query-realms}", + "composite" : false, + "clientRole" : true, + "containerId" : "4882f437-a423-46a1-878c-10616b7d6117", + "attributes" : { } + }, { + "id" : "41419687-e0a3-4a1b-a380-8a200f2ae2a8", + "name" : "impersonation", + "description" : "${role_impersonation}", + "composite" : false, + "clientRole" : true, + "containerId" : "4882f437-a423-46a1-878c-10616b7d6117", + "attributes" : { } + }, { + "id" : "66c8d39f-46de-4861-934e-440998d5e427", + "name" : "query-users", + "description" : "${role_query-users}", + "composite" : false, + "clientRole" : true, + "containerId" : "4882f437-a423-46a1-878c-10616b7d6117", + "attributes" : { } + }, { + "id" : "50d9bd9d-d100-48be-8239-2661a6a13ca7", + "name" : "view-events", + "description" : "${role_view-events}", + "composite" : false, + "clientRole" : true, + "containerId" : "4882f437-a423-46a1-878c-10616b7d6117", + "attributes" : { } + } ], + "security-admin-console" : [ ], + "admin-cli" : [ ], + "account-console" : [ ], + "broker" : [ { + "id" : "cdeea646-d366-479d-b2d2-40853d6d363d", + "name" : "read-token", + "description" : "${role_read-token}", + "composite" : false, + "clientRole" : true, + "containerId" : "e9f5fb23-6688-4a97-897c-fe9f6a370c64", + "attributes" : { } + } ], + "account" : [ { + "id" : "0b90f930-8203-4599-be96-e5a5cac8ee7b", + "name" : "view-applications", + "description" : "${role_view-applications}", + "composite" : false, + "clientRole" : true, + "containerId" : "b8302e03-6c05-4a42-85e9-a46370d2b76b", + "attributes" : { } + }, { + "id" : "2c7d0710-a3d8-4a40-9f2e-8143f2e9e153", + "name" : "view-profile", + "description" : "${role_view-profile}", + "composite" : false, + "clientRole" : true, + "containerId" : "b8302e03-6c05-4a42-85e9-a46370d2b76b", + "attributes" : { } + }, { + "id" : "d3434c63-75fc-4ac2-af54-91ce26ace472", + "name" : "manage-account", + "description" : "${role_manage-account}", + "composite" : true, + "composites" : { + "client" : { + "account" : [ "manage-account-links" ] + } + }, + "clientRole" : true, + "containerId" : "b8302e03-6c05-4a42-85e9-a46370d2b76b", + "attributes" : { } + }, { + "id" : "7846f314-a5f3-4bac-a02c-7da437c300ce", + "name" : "manage-consent", + "description" : "${role_manage-consent}", + "composite" : true, + "composites" : { + "client" : { + "account" : [ "view-consent" ] + } + }, + "clientRole" : true, + "containerId" : "b8302e03-6c05-4a42-85e9-a46370d2b76b", + "attributes" : { } + }, { + "id" : "2f69f35f-6748-4002-aecf-85d194c28d94", + "name" : "manage-account-links", + "description" : "${role_manage-account-links}", + "composite" : false, + "clientRole" : true, + "containerId" : "b8302e03-6c05-4a42-85e9-a46370d2b76b", + "attributes" : { } + }, { + "id" : "1edba5fd-9d7e-4a15-8dcf-3a14a4b738cc", + "name" : "delete-account", + "description" : "${role_delete-account}", + "composite" : false, + "clientRole" : true, + "containerId" : "b8302e03-6c05-4a42-85e9-a46370d2b76b", + "attributes" : { } + }, { + "id" : "ce9b2c54-d838-42ae-bbe6-d6ee70e46902", + "name" : "view-consent", + "description" : "${role_view-consent}", + "composite" : false, + "clientRole" : true, + "containerId" : "b8302e03-6c05-4a42-85e9-a46370d2b76b", + "attributes" : { } + }, { + "id" : "4df2f06c-a286-4f29-bb5e-c5259780099c", + "name" : "view-groups", + "description" : "${role_view-groups}", + "composite" : false, + "clientRole" : true, + "containerId" : "b8302e03-6c05-4a42-85e9-a46370d2b76b", + "attributes" : { } + } ], + "veraison-client" : [ ] + } + }, + "groups" : [ ], + "defaultRole" : { + "id" : "3c85b41b-2cd1-40af-9e31-c6a9d24114ce", + "name" : "default-roles-veraison", + "description" : "${role_default-roles}", + "composite" : true, + "clientRole" : false, + "containerId" : "f1c336ca-84a0-4bbf-b075-d6276bfb8f5e" + }, + "requiredCredentials" : [ "password" ], + "otpPolicyType" : "totp", + "otpPolicyAlgorithm" : "HmacSHA1", + "otpPolicyInitialCounter" : 0, + "otpPolicyDigits" : 6, + "otpPolicyLookAheadWindow" : 1, + "otpPolicyPeriod" : 30, + "otpPolicyCodeReusable" : false, + "otpSupportedApplications" : [ "totpAppMicrosoftAuthenticatorName", "totpAppGoogleName", "totpAppFreeOTPName" ], + "webAuthnPolicyRpEntityName" : "keycloak", + "webAuthnPolicySignatureAlgorithms" : [ "ES256" ], + "webAuthnPolicyRpId" : "", + "webAuthnPolicyAttestationConveyancePreference" : "not specified", + "webAuthnPolicyAuthenticatorAttachment" : "not specified", + "webAuthnPolicyRequireResidentKey" : "not specified", + "webAuthnPolicyUserVerificationRequirement" : "not specified", + "webAuthnPolicyCreateTimeout" : 0, + "webAuthnPolicyAvoidSameAuthenticatorRegister" : false, + "webAuthnPolicyAcceptableAaguids" : [ ], + "webAuthnPolicyPasswordlessRpEntityName" : "keycloak", + "webAuthnPolicyPasswordlessSignatureAlgorithms" : [ "ES256" ], + "webAuthnPolicyPasswordlessRpId" : "", + "webAuthnPolicyPasswordlessAttestationConveyancePreference" : "not specified", + "webAuthnPolicyPasswordlessAuthenticatorAttachment" : "not specified", + "webAuthnPolicyPasswordlessRequireResidentKey" : "not specified", + "webAuthnPolicyPasswordlessUserVerificationRequirement" : "not specified", + "webAuthnPolicyPasswordlessCreateTimeout" : 0, + "webAuthnPolicyPasswordlessAvoidSameAuthenticatorRegister" : false, + "webAuthnPolicyPasswordlessAcceptableAaguids" : [ ], + "users" : [ { + "id" : "778a3424-b538-415c-b9c6-ade0e483d818", + "createdTimestamp" : 1692274136073, + "username" : "veraison-manager", + "enabled" : true, + "totp" : false, + "emailVerified" : false, + "firstName" : "", + "lastName" : "", + "credentials" : [ { + "id" : "086eb891-30fd-408d-91a0-7d4c8a701f54", + "type" : "password", + "userLabel" : "My password", + "createdDate" : 1692274151671, + "secretData" : "{\"value\":\"KDJZv/qvMYMCb5v18ymqGrKy9ZPk/3zB3WvrzwFogRI=\",\"salt\":\"zU0ayGQFxfgyU2T2EU90+w==\",\"additionalParameters\":{}}", + "credentialData" : "{\"hashIterations\":27500,\"algorithm\":\"pbkdf2-sha256\",\"additionalParameters\":{}}" + } ], + "disableableCredentialTypes" : [ ], + "requiredActions" : [ ], + "realmRoles" : [ "manager", "default-roles-veraison" ], + "notBefore" : 0, + "groups" : [ ] + }, { + "id" : "3d0bfa21-50b0-496a-9a22-c6e1f63238f9", + "createdTimestamp" : 1692274044033, + "username" : "veraison-provisioner", + "enabled" : true, + "totp" : false, + "emailVerified" : false, + "firstName" : "", + "lastName" : "", + "credentials" : [ { + "id" : "0dd61192-932a-4972-a168-2c4867673396", + "type" : "password", + "userLabel" : "My password", + "createdDate" : 1692275392983, + "secretData" : "{\"value\":\"+uFdoOr+hk62Z87HGA9RvWcXhJMNX4YHPjmkjJSK16U=\",\"salt\":\"s/dmj1YbJ+/yLdbnmAg/8Q==\",\"additionalParameters\":{}}", + "credentialData" : "{\"hashIterations\":27500,\"algorithm\":\"pbkdf2-sha256\",\"additionalParameters\":{}}" + } ], + "disableableCredentialTypes" : [ ], + "requiredActions" : [ ], + "realmRoles" : [ "provisioner", "default-roles-veraison" ], + "notBefore" : 0, + "groups" : [ ] + } ], + "scopeMappings" : [ { + "clientScope" : "offline_access", + "roles" : [ "offline_access" ] + } ], + "clientScopeMappings" : { + "account" : [ { + "client" : "account-console", + "roles" : [ "manage-account", "view-groups" ] + } ] + }, + "clients" : [ { + "id" : "b8302e03-6c05-4a42-85e9-a46370d2b76b", + "clientId" : "account", + "name" : "${client_account}", + "rootUrl" : "${authBaseUrl}", + "baseUrl" : "/realms/veraison/account/", + "surrogateAuthRequired" : false, + "enabled" : true, + "alwaysDisplayInConsole" : false, + "clientAuthenticatorType" : "client-secret", + "redirectUris" : [ "/realms/veraison/account/*" ], + "webOrigins" : [ ], + "notBefore" : 0, + "bearerOnly" : false, + "consentRequired" : false, + "standardFlowEnabled" : true, + "implicitFlowEnabled" : false, + "directAccessGrantsEnabled" : false, + "serviceAccountsEnabled" : false, + "publicClient" : true, + "frontchannelLogout" : false, + "protocol" : "openid-connect", + "attributes" : { + "post.logout.redirect.uris" : "+" + }, + "authenticationFlowBindingOverrides" : { }, + "fullScopeAllowed" : false, + "nodeReRegistrationTimeout" : 0, + "defaultClientScopes" : [ "web-origins", "acr", "profile", "roles", "email" ], + "optionalClientScopes" : [ "address", "phone", "offline_access", "microprofile-jwt" ] + }, { + "id" : "0124f2b3-be1b-49b5-a113-352d1ffe299a", + "clientId" : "account-console", + "name" : "${client_account-console}", + "rootUrl" : "${authBaseUrl}", + "baseUrl" : "/realms/veraison/account/", + "surrogateAuthRequired" : false, + "enabled" : true, + "alwaysDisplayInConsole" : false, + "clientAuthenticatorType" : "client-secret", + "redirectUris" : [ "/realms/veraison/account/*" ], + "webOrigins" : [ ], + "notBefore" : 0, + "bearerOnly" : false, + "consentRequired" : false, + "standardFlowEnabled" : true, + "implicitFlowEnabled" : false, + "directAccessGrantsEnabled" : false, + "serviceAccountsEnabled" : false, + "publicClient" : true, + "frontchannelLogout" : false, + "protocol" : "openid-connect", + "attributes" : { + "post.logout.redirect.uris" : "+", + "pkce.code.challenge.method" : "S256" + }, + "authenticationFlowBindingOverrides" : { }, + "fullScopeAllowed" : false, + "nodeReRegistrationTimeout" : 0, + "protocolMappers" : [ { + "id" : "3b63fc08-5779-4dc5-a2ce-5a22a201fd49", + "name" : "audience resolve", + "protocol" : "openid-connect", + "protocolMapper" : "oidc-audience-resolve-mapper", + "consentRequired" : false, + "config" : { } + } ], + "defaultClientScopes" : [ "web-origins", "acr", "profile", "roles", "email" ], + "optionalClientScopes" : [ "address", "phone", "offline_access", "microprofile-jwt" ] + }, { + "id" : "b2e061bf-883b-4534-925b-f3aafb433fd7", + "clientId" : "admin-cli", + "name" : "${client_admin-cli}", + "surrogateAuthRequired" : false, + "enabled" : true, + "alwaysDisplayInConsole" : false, + "clientAuthenticatorType" : "client-secret", + "redirectUris" : [ ], + "webOrigins" : [ ], + "notBefore" : 0, + "bearerOnly" : false, + "consentRequired" : false, + "standardFlowEnabled" : false, + "implicitFlowEnabled" : false, + "directAccessGrantsEnabled" : true, + "serviceAccountsEnabled" : false, + "publicClient" : true, + "frontchannelLogout" : false, + "protocol" : "openid-connect", + "attributes" : { + "post.logout.redirect.uris" : "+" + }, + "authenticationFlowBindingOverrides" : { }, + "fullScopeAllowed" : false, + "nodeReRegistrationTimeout" : 0, + "defaultClientScopes" : [ "web-origins", "acr", "profile", "roles", "email" ], + "optionalClientScopes" : [ "address", "phone", "offline_access", "microprofile-jwt" ] + }, { + "id" : "e9f5fb23-6688-4a97-897c-fe9f6a370c64", + "clientId" : "broker", + "name" : "${client_broker}", + "surrogateAuthRequired" : false, + "enabled" : true, + "alwaysDisplayInConsole" : false, + "clientAuthenticatorType" : "client-secret", + "redirectUris" : [ ], + "webOrigins" : [ ], + "notBefore" : 0, + "bearerOnly" : true, + "consentRequired" : false, + "standardFlowEnabled" : true, + "implicitFlowEnabled" : false, + "directAccessGrantsEnabled" : false, + "serviceAccountsEnabled" : false, + "publicClient" : false, + "frontchannelLogout" : false, + "protocol" : "openid-connect", + "attributes" : { + "post.logout.redirect.uris" : "+" + }, + "authenticationFlowBindingOverrides" : { }, + "fullScopeAllowed" : false, + "nodeReRegistrationTimeout" : 0, + "defaultClientScopes" : [ "web-origins", "acr", "profile", "roles", "email" ], + "optionalClientScopes" : [ "address", "phone", "offline_access", "microprofile-jwt" ] + }, { + "id" : "4882f437-a423-46a1-878c-10616b7d6117", + "clientId" : "realm-management", + "name" : "${client_realm-management}", + "surrogateAuthRequired" : false, + "enabled" : true, + "alwaysDisplayInConsole" : false, + "clientAuthenticatorType" : "client-secret", + "redirectUris" : [ ], + "webOrigins" : [ ], + "notBefore" : 0, + "bearerOnly" : true, + "consentRequired" : false, + "standardFlowEnabled" : true, + "implicitFlowEnabled" : false, + "directAccessGrantsEnabled" : false, + "serviceAccountsEnabled" : false, + "publicClient" : false, + "frontchannelLogout" : false, + "protocol" : "openid-connect", + "attributes" : { + "post.logout.redirect.uris" : "+" + }, + "authenticationFlowBindingOverrides" : { }, + "fullScopeAllowed" : false, + "nodeReRegistrationTimeout" : 0, + "defaultClientScopes" : [ "web-origins", "acr", "profile", "roles", "email" ], + "optionalClientScopes" : [ "address", "phone", "offline_access", "microprofile-jwt" ] + }, { + "id" : "9fc0fda8-3c5e-46d4-ae64-126fa11ecbb3", + "clientId" : "security-admin-console", + "name" : "${client_security-admin-console}", + "rootUrl" : "${authAdminUrl}", + "baseUrl" : "/admin/veraison/console/", + "surrogateAuthRequired" : false, + "enabled" : true, + "alwaysDisplayInConsole" : false, + "clientAuthenticatorType" : "client-secret", + "redirectUris" : [ "/admin/veraison/console/*" ], + "webOrigins" : [ "+" ], + "notBefore" : 0, + "bearerOnly" : false, + "consentRequired" : false, + "standardFlowEnabled" : true, + "implicitFlowEnabled" : false, + "directAccessGrantsEnabled" : false, + "serviceAccountsEnabled" : false, + "publicClient" : true, + "frontchannelLogout" : false, + "protocol" : "openid-connect", + "attributes" : { + "post.logout.redirect.uris" : "+", + "pkce.code.challenge.method" : "S256" + }, + "authenticationFlowBindingOverrides" : { }, + "fullScopeAllowed" : false, + "nodeReRegistrationTimeout" : 0, + "protocolMappers" : [ { + "id" : "7bd436f6-4a54-425e-bb6c-7de806fcf84d", + "name" : "locale", + "protocol" : "openid-connect", + "protocolMapper" : "oidc-usermodel-attribute-mapper", + "consentRequired" : false, + "config" : { + "userinfo.token.claim" : "true", + "user.attribute" : "locale", + "id.token.claim" : "true", + "access.token.claim" : "true", + "claim.name" : "locale", + "jsonType.label" : "String" + } + } ], + "defaultClientScopes" : [ "web-origins", "acr", "profile", "roles", "email" ], + "optionalClientScopes" : [ "address", "phone", "offline_access", "microprofile-jwt" ] + }, { + "id" : "30ea0b69-30fe-46bd-82f7-ac1f980b6928", + "clientId" : "veraison-client", + "name" : "", + "description" : "", + "rootUrl" : "", + "adminUrl" : "", + "baseUrl" : "", + "surrogateAuthRequired" : false, + "enabled" : true, + "alwaysDisplayInConsole" : true, + "clientAuthenticatorType" : "client-secret", + "secret" : "YifmabB4cVSPPtFLAmHfq7wKaEHQn10Z", + "redirectUris" : [ "/*" ], + "webOrigins" : [ "/*" ], + "notBefore" : 0, + "bearerOnly" : false, + "consentRequired" : false, + "standardFlowEnabled" : true, + "implicitFlowEnabled" : false, + "directAccessGrantsEnabled" : true, + "serviceAccountsEnabled" : false, + "publicClient" : false, + "frontchannelLogout" : true, + "protocol" : "openid-connect", + "attributes" : { + "oidc.ciba.grant.enabled" : "false", + "client.secret.creation.time" : "1692267068", + "backchannel.logout.session.required" : "true", + "post.logout.redirect.uris" : "+", + "oauth2.device.authorization.grant.enabled" : "false", + "display.on.consent.screen" : "false", + "backchannel.logout.revoke.offline.tokens" : "false" + }, + "authenticationFlowBindingOverrides" : { }, + "fullScopeAllowed" : true, + "nodeReRegistrationTimeout" : -1, + "defaultClientScopes" : [ "web-origins", "acr", "profile", "roles", "email" ], + "optionalClientScopes" : [ "address", "phone", "offline_access", "microprofile-jwt" ] + } ], + "clientScopes" : [ { + "id" : "cdb2454d-355f-46e6-8c75-5ee7d104011d", + "name" : "profile", + "description" : "OpenID Connect built-in scope: profile", + "protocol" : "openid-connect", + "attributes" : { + "include.in.token.scope" : "true", + "display.on.consent.screen" : "true", + "consent.screen.text" : "${profileScopeConsentText}" + }, + "protocolMappers" : [ { + "id" : "5a0ee988-c3b5-400d-a77a-b6db95b44583", + "name" : "zoneinfo", + "protocol" : "openid-connect", + "protocolMapper" : "oidc-usermodel-attribute-mapper", + "consentRequired" : false, + "config" : { + "userinfo.token.claim" : "true", + "user.attribute" : "zoneinfo", + "id.token.claim" : "true", + "access.token.claim" : "true", + "claim.name" : "zoneinfo", + "jsonType.label" : "String" + } + }, { + "id" : "57f0aa40-359f-4f54-8413-1c414d95dc62", + "name" : "given name", + "protocol" : "openid-connect", + "protocolMapper" : "oidc-usermodel-attribute-mapper", + "consentRequired" : false, + "config" : { + "userinfo.token.claim" : "true", + "user.attribute" : "firstName", + "id.token.claim" : "true", + "access.token.claim" : "true", + "claim.name" : "given_name", + "jsonType.label" : "String" + } + }, { + "id" : "2fb49e8e-fceb-4ebf-a4f8-b7b074d076fb", + "name" : "nickname", + "protocol" : "openid-connect", + "protocolMapper" : "oidc-usermodel-attribute-mapper", + "consentRequired" : false, + "config" : { + "userinfo.token.claim" : "true", + "user.attribute" : "nickname", + "id.token.claim" : "true", + "access.token.claim" : "true", + "claim.name" : "nickname", + "jsonType.label" : "String" + } + }, { + "id" : "fe6d8736-c643-4189-bff0-22179ccd0173", + "name" : "gender", + "protocol" : "openid-connect", + "protocolMapper" : "oidc-usermodel-attribute-mapper", + "consentRequired" : false, + "config" : { + "userinfo.token.claim" : "true", + "user.attribute" : "gender", + "id.token.claim" : "true", + "access.token.claim" : "true", + "claim.name" : "gender", + "jsonType.label" : "String" + } + }, { + "id" : "1407f674-94da-4baf-85a7-4825673df97a", + "name" : "full name", + "protocol" : "openid-connect", + "protocolMapper" : "oidc-full-name-mapper", + "consentRequired" : false, + "config" : { + "id.token.claim" : "true", + "access.token.claim" : "true", + "userinfo.token.claim" : "true" + } + }, { + "id" : "7d79f487-aa57-414e-b68f-b731056aef2a", + "name" : "birthdate", + "protocol" : "openid-connect", + "protocolMapper" : "oidc-usermodel-attribute-mapper", + "consentRequired" : false, + "config" : { + "userinfo.token.claim" : "true", + "user.attribute" : "birthdate", + "id.token.claim" : "true", + "access.token.claim" : "true", + "claim.name" : "birthdate", + "jsonType.label" : "String" + } + }, { + "id" : "d2296431-072a-4bf1-87be-6e0f390e255a", + "name" : "middle name", + "protocol" : "openid-connect", + "protocolMapper" : "oidc-usermodel-attribute-mapper", + "consentRequired" : false, + "config" : { + "userinfo.token.claim" : "true", + "user.attribute" : "middleName", + "id.token.claim" : "true", + "access.token.claim" : "true", + "claim.name" : "middle_name", + "jsonType.label" : "String" + } + }, { + "id" : "162ede80-8a96-47ae-9671-0c6eb1bfe520", + "name" : "username", + "protocol" : "openid-connect", + "protocolMapper" : "oidc-usermodel-attribute-mapper", + "consentRequired" : false, + "config" : { + "userinfo.token.claim" : "true", + "user.attribute" : "username", + "id.token.claim" : "true", + "access.token.claim" : "true", + "claim.name" : "preferred_username", + "jsonType.label" : "String" + } + }, { + "id" : "c93b9d49-5826-45ac-b05a-c4c5fafa5b96", + "name" : "family name", + "protocol" : "openid-connect", + "protocolMapper" : "oidc-usermodel-attribute-mapper", + "consentRequired" : false, + "config" : { + "userinfo.token.claim" : "true", + "user.attribute" : "lastName", + "id.token.claim" : "true", + "access.token.claim" : "true", + "claim.name" : "family_name", + "jsonType.label" : "String" + } + }, { + "id" : "e7cf775c-3af5-4ae4-aaf4-85a84999d064", + "name" : "picture", + "protocol" : "openid-connect", + "protocolMapper" : "oidc-usermodel-attribute-mapper", + "consentRequired" : false, + "config" : { + "userinfo.token.claim" : "true", + "user.attribute" : "picture", + "id.token.claim" : "true", + "access.token.claim" : "true", + "claim.name" : "picture", + "jsonType.label" : "String" + } + }, { + "id" : "15c2e269-faff-4013-a68d-ed20ea46031e", + "name" : "locale", + "protocol" : "openid-connect", + "protocolMapper" : "oidc-usermodel-attribute-mapper", + "consentRequired" : false, + "config" : { + "userinfo.token.claim" : "true", + "user.attribute" : "locale", + "id.token.claim" : "true", + "access.token.claim" : "true", + "claim.name" : "locale", + "jsonType.label" : "String" + } + }, { + "id" : "70ad35eb-7736-487d-b4c0-42fd4fe0d563", + "name" : "updated at", + "protocol" : "openid-connect", + "protocolMapper" : "oidc-usermodel-attribute-mapper", + "consentRequired" : false, + "config" : { + "userinfo.token.claim" : "true", + "user.attribute" : "updatedAt", + "id.token.claim" : "true", + "access.token.claim" : "true", + "claim.name" : "updated_at", + "jsonType.label" : "long" + } + }, { + "id" : "b4b48ad4-f324-47d9-a0dc-7d5a47c5126d", + "name" : "website", + "protocol" : "openid-connect", + "protocolMapper" : "oidc-usermodel-attribute-mapper", + "consentRequired" : false, + "config" : { + "userinfo.token.claim" : "true", + "user.attribute" : "website", + "id.token.claim" : "true", + "access.token.claim" : "true", + "claim.name" : "website", + "jsonType.label" : "String" + } + }, { + "id" : "ed92a954-3a49-4cda-ad97-c5c605639344", + "name" : "profile", + "protocol" : "openid-connect", + "protocolMapper" : "oidc-usermodel-attribute-mapper", + "consentRequired" : false, + "config" : { + "userinfo.token.claim" : "true", + "user.attribute" : "profile", + "id.token.claim" : "true", + "access.token.claim" : "true", + "claim.name" : "profile", + "jsonType.label" : "String" + } + } ] + }, { + "id" : "622a036e-ec1b-4591-8304-38f6383d5405", + "name" : "offline_access", + "description" : "OpenID Connect built-in scope: offline_access", + "protocol" : "openid-connect", + "attributes" : { + "consent.screen.text" : "${offlineAccessScopeConsentText}", + "display.on.consent.screen" : "true" + } + }, { + "id" : "a53cf6f7-1ecc-403d-9574-f0691388e401", + "name" : "web-origins", + "description" : "OpenID Connect scope for add allowed web origins to the access token", + "protocol" : "openid-connect", + "attributes" : { + "include.in.token.scope" : "false", + "display.on.consent.screen" : "false", + "consent.screen.text" : "" + }, + "protocolMappers" : [ { + "id" : "798ddcba-546b-452d-9d15-27cd5b12b1be", + "name" : "allowed web origins", + "protocol" : "openid-connect", + "protocolMapper" : "oidc-allowed-origins-mapper", + "consentRequired" : false, + "config" : { } + } ] + }, { + "id" : "801544eb-e426-478f-b9f1-d9292d521147", + "name" : "address", + "description" : "OpenID Connect built-in scope: address", + "protocol" : "openid-connect", + "attributes" : { + "include.in.token.scope" : "true", + "display.on.consent.screen" : "true", + "consent.screen.text" : "${addressScopeConsentText}" + }, + "protocolMappers" : [ { + "id" : "7461381f-3dbf-49d7-b033-619b0148ca09", + "name" : "address", + "protocol" : "openid-connect", + "protocolMapper" : "oidc-address-mapper", + "consentRequired" : false, + "config" : { + "user.attribute.formatted" : "formatted", + "user.attribute.country" : "country", + "user.attribute.postal_code" : "postal_code", + "userinfo.token.claim" : "true", + "user.attribute.street" : "street", + "id.token.claim" : "true", + "user.attribute.region" : "region", + "access.token.claim" : "true", + "user.attribute.locality" : "locality" + } + } ] + }, { + "id" : "01d158bf-8c4f-4b3d-b84a-141e114c0b8f", + "name" : "role_list", + "description" : "SAML role list", + "protocol" : "saml", + "attributes" : { + "consent.screen.text" : "${samlRoleListScopeConsentText}", + "display.on.consent.screen" : "true" + }, + "protocolMappers" : [ { + "id" : "4eb1eb4a-5f27-434b-a141-a9f6902d93d5", + "name" : "role list", + "protocol" : "saml", + "protocolMapper" : "saml-role-list-mapper", + "consentRequired" : false, + "config" : { + "single" : "false", + "attribute.nameformat" : "Basic", + "attribute.name" : "Role" + } + } ] + }, { + "id" : "3f241149-6525-4a09-89da-649094ab0e47", + "name" : "email", + "description" : "OpenID Connect built-in scope: email", + "protocol" : "openid-connect", + "attributes" : { + "include.in.token.scope" : "true", + "display.on.consent.screen" : "true", + "consent.screen.text" : "${emailScopeConsentText}" + }, + "protocolMappers" : [ { + "id" : "43909b68-6505-4119-abd9-df8447ab6dfc", + "name" : "email", + "protocol" : "openid-connect", + "protocolMapper" : "oidc-usermodel-attribute-mapper", + "consentRequired" : false, + "config" : { + "userinfo.token.claim" : "true", + "user.attribute" : "email", + "id.token.claim" : "true", + "access.token.claim" : "true", + "claim.name" : "email", + "jsonType.label" : "String" + } + }, { + "id" : "1caf557c-c8c4-4e5e-bf42-20365baa40e1", + "name" : "email verified", + "protocol" : "openid-connect", + "protocolMapper" : "oidc-usermodel-property-mapper", + "consentRequired" : false, + "config" : { + "userinfo.token.claim" : "true", + "user.attribute" : "emailVerified", + "id.token.claim" : "true", + "access.token.claim" : "true", + "claim.name" : "email_verified", + "jsonType.label" : "boolean" + } + } ] + }, { + "id" : "c8fa1b3b-d9ef-4fef-b291-35318d6817ad", + "name" : "roles", + "description" : "OpenID Connect scope for add user roles to the access token", + "protocol" : "openid-connect", + "attributes" : { + "include.in.token.scope" : "false", + "display.on.consent.screen" : "true", + "consent.screen.text" : "${rolesScopeConsentText}" + }, + "protocolMappers" : [ { + "id" : "e1fbb06d-9b7f-4e2f-ae8c-a2d0cd41dc76", + "name" : "realm roles", + "protocol" : "openid-connect", + "protocolMapper" : "oidc-usermodel-realm-role-mapper", + "consentRequired" : false, + "config" : { + "user.attribute" : "foo", + "access.token.claim" : "true", + "claim.name" : "realm_access.roles", + "jsonType.label" : "String", + "multivalued" : "true" + } + }, { + "id" : "b3875b32-f72d-4fed-ac28-fe48b94261bf", + "name" : "audience resolve", + "protocol" : "openid-connect", + "protocolMapper" : "oidc-audience-resolve-mapper", + "consentRequired" : false, + "config" : { } + }, { + "id" : "e9094f82-a0ed-47e2-b484-377e55e24d68", + "name" : "client roles", + "protocol" : "openid-connect", + "protocolMapper" : "oidc-usermodel-client-role-mapper", + "consentRequired" : false, + "config" : { + "user.attribute" : "foo", + "access.token.claim" : "true", + "claim.name" : "resource_access.${client_id}.roles", + "jsonType.label" : "String", + "multivalued" : "true" + } + } ] + }, { + "id" : "23735c41-4b70-46b0-8bd8-66d6db6ae200", + "name" : "microprofile-jwt", + "description" : "Microprofile - JWT built-in scope", + "protocol" : "openid-connect", + "attributes" : { + "include.in.token.scope" : "true", + "display.on.consent.screen" : "false" + }, + "protocolMappers" : [ { + "id" : "4d7eb99e-09bb-450f-9f22-324d52dc58da", + "name" : "groups", + "protocol" : "openid-connect", + "protocolMapper" : "oidc-usermodel-realm-role-mapper", + "consentRequired" : false, + "config" : { + "multivalued" : "true", + "userinfo.token.claim" : "true", + "user.attribute" : "foo", + "id.token.claim" : "true", + "access.token.claim" : "true", + "claim.name" : "groups", + "jsonType.label" : "String" + } + }, { + "id" : "70c1a038-13c6-405a-8e90-ea23930a7ff3", + "name" : "upn", + "protocol" : "openid-connect", + "protocolMapper" : "oidc-usermodel-attribute-mapper", + "consentRequired" : false, + "config" : { + "userinfo.token.claim" : "true", + "user.attribute" : "username", + "id.token.claim" : "true", + "access.token.claim" : "true", + "claim.name" : "upn", + "jsonType.label" : "String" + } + } ] + }, { + "id" : "e24a3896-7bc6-4007-9582-852cf918aaab", + "name" : "phone", + "description" : "OpenID Connect built-in scope: phone", + "protocol" : "openid-connect", + "attributes" : { + "include.in.token.scope" : "true", + "display.on.consent.screen" : "true", + "consent.screen.text" : "${phoneScopeConsentText}" + }, + "protocolMappers" : [ { + "id" : "523c6a94-87a6-4022-bdbf-842b2b2a2ac4", + "name" : "phone number verified", + "protocol" : "openid-connect", + "protocolMapper" : "oidc-usermodel-attribute-mapper", + "consentRequired" : false, + "config" : { + "userinfo.token.claim" : "true", + "user.attribute" : "phoneNumberVerified", + "id.token.claim" : "true", + "access.token.claim" : "true", + "claim.name" : "phone_number_verified", + "jsonType.label" : "boolean" + } + }, { + "id" : "4adfae7f-d902-467f-91a0-22bbfa3b845f", + "name" : "phone number", + "protocol" : "openid-connect", + "protocolMapper" : "oidc-usermodel-attribute-mapper", + "consentRequired" : false, + "config" : { + "userinfo.token.claim" : "true", + "user.attribute" : "phoneNumber", + "id.token.claim" : "true", + "access.token.claim" : "true", + "claim.name" : "phone_number", + "jsonType.label" : "String" + } + } ] + }, { + "id" : "a867d920-baaf-424b-8e56-8c941f79bbd9", + "name" : "acr", + "description" : "OpenID Connect scope for add acr (authentication context class reference) to the token", + "protocol" : "openid-connect", + "attributes" : { + "include.in.token.scope" : "false", + "display.on.consent.screen" : "false" + }, + "protocolMappers" : [ { + "id" : "da651414-ffb1-4e47-a2ca-cdd1c6270481", + "name" : "acr loa level", + "protocol" : "openid-connect", + "protocolMapper" : "oidc-acr-mapper", + "consentRequired" : false, + "config" : { + "id.token.claim" : "true", + "access.token.claim" : "true", + "userinfo.token.claim" : "true" + } + } ] + } ], + "defaultDefaultClientScopes" : [ "role_list", "profile", "email", "roles", "web-origins", "acr" ], + "defaultOptionalClientScopes" : [ "offline_access", "address", "phone", "microprofile-jwt" ], + "browserSecurityHeaders" : { + "contentSecurityPolicyReportOnly" : "", + "xContentTypeOptions" : "nosniff", + "referrerPolicy" : "no-referrer", + "xRobotsTag" : "none", + "xFrameOptions" : "SAMEORIGIN", + "contentSecurityPolicy" : "frame-src 'self'; frame-ancestors 'self'; object-src 'none';", + "xXSSProtection" : "1; mode=block", + "strictTransportSecurity" : "max-age=31536000; includeSubDomains" + }, + "smtpServer" : { }, + "eventsEnabled" : false, + "eventsListeners" : [ "jboss-logging" ], + "enabledEventTypes" : [ ], + "adminEventsEnabled" : false, + "adminEventsDetailsEnabled" : false, + "identityProviders" : [ ], + "identityProviderMappers" : [ ], + "components" : { + "org.keycloak.services.clientregistration.policy.ClientRegistrationPolicy" : [ { + "id" : "838db39f-26e9-49f4-a032-522e8a817212", + "name" : "Allowed Client Scopes", + "providerId" : "allowed-client-templates", + "subType" : "authenticated", + "subComponents" : { }, + "config" : { + "allow-default-scopes" : [ "true" ] + } + }, { + "id" : "5516ee07-cd4c-40c2-8ee0-c39010f6c521", + "name" : "Allowed Protocol Mapper Types", + "providerId" : "allowed-protocol-mappers", + "subType" : "anonymous", + "subComponents" : { }, + "config" : { + "allowed-protocol-mapper-types" : [ "saml-user-property-mapper", "oidc-usermodel-attribute-mapper", "saml-user-attribute-mapper", "oidc-address-mapper", "oidc-sha256-pairwise-sub-mapper", "oidc-full-name-mapper", "saml-role-list-mapper", "oidc-usermodel-property-mapper" ] + } + }, { + "id" : "961ce4e7-198c-49bc-b988-6b87f313b89b", + "name" : "Max Clients Limit", + "providerId" : "max-clients", + "subType" : "anonymous", + "subComponents" : { }, + "config" : { + "max-clients" : [ "200" ] + } + }, { + "id" : "ad3a8ca5-9bd9-4c97-a7c7-ec04a2ad6279", + "name" : "Consent Required", + "providerId" : "consent-required", + "subType" : "anonymous", + "subComponents" : { }, + "config" : { } + }, { + "id" : "875ae783-21e0-4971-8643-e0d9dfbce3e1", + "name" : "Allowed Protocol Mapper Types", + "providerId" : "allowed-protocol-mappers", + "subType" : "authenticated", + "subComponents" : { }, + "config" : { + "allowed-protocol-mapper-types" : [ "oidc-address-mapper", "oidc-sha256-pairwise-sub-mapper", "saml-role-list-mapper", "oidc-usermodel-attribute-mapper", "oidc-full-name-mapper", "saml-user-attribute-mapper", "saml-user-property-mapper", "oidc-usermodel-property-mapper" ] + } + }, { + "id" : "4bce5375-223e-41cd-af02-1d3813895b07", + "name" : "Full Scope Disabled", + "providerId" : "scope", + "subType" : "anonymous", + "subComponents" : { }, + "config" : { } + }, { + "id" : "23ace297-c838-4fa3-ae94-3f0e01d257d5", + "name" : "Allowed Client Scopes", + "providerId" : "allowed-client-templates", + "subType" : "anonymous", + "subComponents" : { }, + "config" : { + "allow-default-scopes" : [ "true" ] + } + }, { + "id" : "69da926b-3bb5-433c-80bd-906a4180a8bf", + "name" : "Trusted Hosts", + "providerId" : "trusted-hosts", + "subType" : "anonymous", + "subComponents" : { }, + "config" : { + "host-sending-registration-request-must-match" : [ "true" ], + "client-uris-must-match" : [ "true" ] + } + } ], + "org.keycloak.keys.KeyProvider" : [ { + "id" : "94bc520a-7994-42a9-8083-3c185e66eca8", + "name" : "hmac-generated", + "providerId" : "hmac-generated", + "subComponents" : { }, + "config" : { + "kid" : [ "a7e63c45-88cb-4a77-a458-a55473909d17" ], + "secret" : [ "tbWy-FjQ4PPZClLRCByN5PAWOh5oHwlPjJklDF-WyYpr4WCQnfBnUPnBdEI6ThyGEZ1oSGEq3wnivoQMARYLOA" ], + "priority" : [ "100" ], + "algorithm" : [ "HS256" ] + } + }, { + "id" : "ea51c427-9158-4564-ad9c-fccff828ea2a", + "name" : "rsa-enc-generated", + "providerId" : "rsa-enc-generated", + "subComponents" : { }, + "config" : { + "privateKey" : [ "MIIEpQIBAAKCAQEAwQ1evDFc9jC1e6SZuM4oAVs+O1Mj8iXK6otj4aYG5ox2I9Vg2AjaF53oX0b9PIUyvkdxpPYBPIqkfe7P6woXjOZEiUo/vQfR79qdjwjfWLhOGuLezj2OQjAEzRdg4A2LhMDYv8rc5S3aTJ1Q7hdXe+ETDg6o/2ou/IJyIXcz9HDnIelrtPTemmQbrSFvYh8NLJwLqLfa4KxCVnUiwOGWed6L96lV8Z6W+R7ssN/XekaPf8fsfxXn4DFU7mGHEpjEfInUHg7oQMSFotCbDFg1JFKLRIFOZvo3690cgdf/VaeWvYuc5WEK+hEHq2POXIYTpUtNndGasdKQBnhY88ZZqwIDAQABAoIBAAWMOCVkWG2NZ3RkKIlnsz1GvU/AdeSm3WFGxtoHMWToCG/DLq3xTdh75d+cZKPxQqIL34zZXPO7xY+w5dy1RN6nB1/X+GOa7peGoBx/fsZH6vFZhQRG6ehRBPJNMlrTSNJ0evDCsv0LB55IBGQIhlpoVL8vVf/xBzn5GFqJ2GkKgMdUTVeLIOt48V/raoN4lS0g8jJMypOorXs17JLPKsECOFm4VKKzYQ/S5JHi/Cq9PcSex+V2nD/blh5FqbVWYxGP5hFPHx0N37Hi8fxZAXAT8mHShNyT7NnPsdgy6kEJ09tET9KP3A7ymlzXD8BXoWEKBMvvepfyle4GTNxDuuECgYEA46+2yQaAINnKMzFcI4Smk8H8vLM3lXl5bXWBqYTx4FeB0VOsLBn46IOu8pxJw2nx2htPmKUDkcQZO0NqOz3MTIxVgjwidRnzmp3p0K4GDeKdnO7KGyDxScQxqNl4zuQc/l15YfQN09jBxt6WtjF+qXUV6qGiNHGkQFxMRF4T6LMCgYEA2Q8YLIjgJybWsHQ7fvHNNMQjQGGRAdSWRHJLfpJU/icfjrLCB/3i+Pc17UmovRtJIYUzHVs54rAB5tl7yNc7g0XMhTCxCV/0ETPYlKL3Yb18aZvSvwp7R3Nxru/y1l/E1R9vyF8bwzVthe2Any53Ru/cJhhUknXZPHYI4++aFykCgYEA1kI8V9/uIvvP82y3sBTcTJ94HnroC5lMU10Ir2WT1/GBEGMU2kt2mBeTQmsgXuwL05tvw81FFp7av5IpHaaB4mcM8Il2Q4wwWYfQx7d7qwVeHJf5SJ4vcaNWt/YuYUL4pcWAvFTVzk1jzKkaWkkpUH4GKc4AvilSz94LRyrgwVECgYEArAnytxml0GJQN3tozK0KYJA1AIpUTIcasxWEEMYa53ZK5Od6MqtggsQt0e1X+Mrvo8nXQaVUs/+dAkPOgNlXKizgdZCqQSv0Xs4hE243dRiiy3HeD91W6MLvkCBO8OrnL6TDDKWVc5udO1GLaJ+Dmo3yh58xKQSPMgS79y2pjEECgYEA2yjd8vfqRVcsnz6LimX2YIMIlRYd867de4sEIhJHSfXspxD5UgVj9cxn6BUvzAafFH1cFV+LR7DU4IjgBsWsYWYq1Kt1SXrrxwh7LmYT6PX2z7jfk6jRcpsZPNJdCDqmW03Ex+xLGUKKj7C5vMhe/fZqV383rGTKPuO81AVxjKI=" ], + "certificate" : [ "MIICnzCCAYcCBgGKA2Q2rTANBgkqhkiG9w0BAQsFADATMREwDwYDVQQDDAh2ZXJhaXNvbjAeFw0yMzA4MTcxMjA1MTNaFw0zMzA4MTcxMjA2NTNaMBMxETAPBgNVBAMMCHZlcmFpc29uMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAwQ1evDFc9jC1e6SZuM4oAVs+O1Mj8iXK6otj4aYG5ox2I9Vg2AjaF53oX0b9PIUyvkdxpPYBPIqkfe7P6woXjOZEiUo/vQfR79qdjwjfWLhOGuLezj2OQjAEzRdg4A2LhMDYv8rc5S3aTJ1Q7hdXe+ETDg6o/2ou/IJyIXcz9HDnIelrtPTemmQbrSFvYh8NLJwLqLfa4KxCVnUiwOGWed6L96lV8Z6W+R7ssN/XekaPf8fsfxXn4DFU7mGHEpjEfInUHg7oQMSFotCbDFg1JFKLRIFOZvo3690cgdf/VaeWvYuc5WEK+hEHq2POXIYTpUtNndGasdKQBnhY88ZZqwIDAQABMA0GCSqGSIb3DQEBCwUAA4IBAQBykUMKuwsNci+zvJdnRPINREzBxVJGbpwJTYdWT8dNYHLakP0HhRXDqAzWdBQkuqSTroUjCSxKsLfnoon3dAQkYM/64yCHFo13a1wtg0pJw0o8BTM5j2TrjnFWvj7XR0xiRGpj2yz0as4gN29Gj3Atw1IEkBnSO4BySEyTou7fGEqWcE6OUWAeyA5OCuCs7geSbghNNCrCMnyKAGLIgidGI7CgopZMQqJ/UoiPXHNuor+N3TMiDYZ3knsi9jY8bJ1DixbkjOVmaG3VI7z5iwNF7jrOpY7XR9SMKt32QFxf1VXMGzygiBoRPIpt6OENGO4naJTIAVLwaZxaUxClOz+G" ], + "priority" : [ "100" ], + "algorithm" : [ "RSA-OAEP" ] + } + }, { + "id" : "af7c0bdc-1ea0-4c0a-9a95-035e49102c23", + "name" : "rsa-generated", + "providerId" : "rsa-generated", + "subComponents" : { }, + "config" : { + "privateKey" : [ "MIIEpAIBAAKCAQEApJR7Ia2PCedukU9Rsb4CPlzs4UNiAIk5ou/4bTLEMQx8Cb6P4UNJoG95Wpf8jsaZtRUVei+SB7BWLfcPi4w7x6JRhrS67vOxH+P4BbtMqdg5aID/fCxUTMJMiSWx9R39PC6mFzc4ZvxzBvNmIbBBiFWuIHeYZOR/3lcrLTh1r9pCYywRzGPbY0u/8CWDxaQJGrNSGjnEXqL97TTuMuu2t07XJAglPsJR63fxOqgd5RET39uDHQqbLy7bxNiCq9+vMcF6JTr13KYDHmtb4qPT95zoqhRHrCI7EI+ruozpoQG6OV/IOXGJhYyFBjMZLCOIMWWoX510QPmQyRNwHsUT3wIDAQABAoIBABpmLXYSe+jlHumKD/BIf92hqZGHQyITi5OPld7A5OglONmV9UhMiHPb+FryxDKMDWhsMFzADVbUZI53nHO66R/gRomAAKLcTg4afX9AiE86KBiTkJJKHgo4pG7fWrNBveFjGNs3zVafqefJ2m5kR668Z5488MybkR3eB5v3ptEFGgU2ruCFbcLSLM0Rg5MwvXuxNrBFLLvQFsAatofFuDEcGHEXX6Rc+3WY8zK/rvFPhB1O2hCuoj+Ka2NcjEQgetlK4KKYBm42mwV0vqNE6t8ewR3+MvJnr6fWOfTlqR+UQ4BbPEteXrV8Hwi4hSAWrrr9MZgBBzUeKroSHgsAUrkCgYEAzhDK4FQCuEGCoEFVKGGD996qXI1j+yLXC4QrIqHyu8Al+GNpJwVgsUWsF9MfhVNjtctBb2qkIdwfV3nlAA2Sg8gHSDVWA5hAgK8d05qCj3aEuxk/xkb2naluuAvztycVaub7Wn54hq8pY3wOifgMl4Qz+7FoAxFNv8HAgrgIJxUCgYEAzHYk7VXjmiTY9DEXXnd6SugVZu5WTSqykPB7nFfosIIdgi0uInFHdhDMdHR97dW7GZkMB1p8zoVdSrJaU6iiDelfbXFX69tQtPbi3eVqls5htUuvozCNyz7eeZnHQ2EKsxpIZC+OOWHTJbUtGDYRG6yvOqqGR1ZTUNUB9g03zCMCgYB8ABTlKwi78gf2AXqKIywzo1Um/ppUjGGVd4Ixg/y6SGVQ9BlZtt25rzBg4dXM+CI/SkFlF2oPShO+IwbPolsxW9Qt+pJ49UyTY01ygT7hr7Mtl4MOALP0qfmLXP3aj/VOcBJ/IS3L9mnUiNmC4rZJEu/pHJd3iRkdNC1xO+cEBQKBgQCGenqFQ8Wkn/G2gwds0ca0t/tDrSVEMf4qyJF03nkkhyAje9XpP3qSFDB1tB0Trk0WZAx+VazbJOqcc7xnY/XakpF6aV87uQ9XRz8mVXuK3wly9en6ure4Y4xujI98KLqh3HqaspCn+0imd4jGcOFFw4mpW3lgOE4qTz+v9zeo4wKBgQCNdhsXJs1gGwgwr5cyUA6XWvoXt5XaQPbSDe8g1h5Va4K28aU3Z+FoDrq6xCkb7klk38S0grQmykGXnVKw/6R09I3WRwe7U9OGjxiN99jDG/wc0qIxdW2XxlvPgphi4AR/XbD16hvhVHecL3p4frjJAu2kp7GFieNcVFPjtcQm2g==" ], + "certificate" : [ "MIICnzCCAYcCBgGKA2Q3iTANBgkqhkiG9w0BAQsFADATMREwDwYDVQQDDAh2ZXJhaXNvbjAeFw0yMzA4MTcxMjA1MTRaFw0zMzA4MTcxMjA2NTRaMBMxETAPBgNVBAMMCHZlcmFpc29uMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEApJR7Ia2PCedukU9Rsb4CPlzs4UNiAIk5ou/4bTLEMQx8Cb6P4UNJoG95Wpf8jsaZtRUVei+SB7BWLfcPi4w7x6JRhrS67vOxH+P4BbtMqdg5aID/fCxUTMJMiSWx9R39PC6mFzc4ZvxzBvNmIbBBiFWuIHeYZOR/3lcrLTh1r9pCYywRzGPbY0u/8CWDxaQJGrNSGjnEXqL97TTuMuu2t07XJAglPsJR63fxOqgd5RET39uDHQqbLy7bxNiCq9+vMcF6JTr13KYDHmtb4qPT95zoqhRHrCI7EI+ruozpoQG6OV/IOXGJhYyFBjMZLCOIMWWoX510QPmQyRNwHsUT3wIDAQABMA0GCSqGSIb3DQEBCwUAA4IBAQA/zFEkpuhffnzxjKRZU05qjA0fEOp7t8C6YI0ERd+bhp0wS5thicSCPhTZgIe3RH23Ozd8lbsmBpnqsOg2PGw/mVjFcutTZrHObCYmmAIQBYlfwfC4UqIx5UjjY+3H/mdsS9XsUAtR9OIuK8NBTSZqHTIK56ifYuifvpVUVbotqM+Yg4r1D6lPBwsvo9OdRxKODrEocfwmMjUJfNX4p1ywXE8sazlBm0CbHASugLEBRU5fQAig+D6RyEijPBXKGrVJxXOwEGO/nukkgmPYruPwNy+h3WmURwkQ5IbnvTBacu9iTYMN/vaRRq1imHNWnjFbZnDCJr+TWIPbVQjY6FY4" ], + "priority" : [ "100" ] + } + }, { + "id" : "1351fa5d-2a4a-4780-8fae-d3927b012acb", + "name" : "aes-generated", + "providerId" : "aes-generated", + "subComponents" : { }, + "config" : { + "kid" : [ "b3f2725f-76e5-4f0d-9b86-328a2995ef85" ], + "secret" : [ "CC51rVYsGuwM7wnkjylqcA" ], + "priority" : [ "100" ] + } + } ] + }, + "internationalizationEnabled" : false, + "supportedLocales" : [ ], + "authenticationFlows" : [ { + "id" : "2015fa69-e7e2-47f2-85a9-72083f2d5799", + "alias" : "Account verification options", + "description" : "Method with which to verity the existing account", + "providerId" : "basic-flow", + "topLevel" : false, + "builtIn" : true, + "authenticationExecutions" : [ { + "authenticator" : "idp-email-verification", + "authenticatorFlow" : false, + "requirement" : "ALTERNATIVE", + "priority" : 10, + "autheticatorFlow" : false, + "userSetupAllowed" : false + }, { + "authenticatorFlow" : true, + "requirement" : "ALTERNATIVE", + "priority" : 20, + "autheticatorFlow" : true, + "flowAlias" : "Verify Existing Account by Re-authentication", + "userSetupAllowed" : false + } ] + }, { + "id" : "c33ebc38-f2c7-4f51-a53e-1ba9370f670a", + "alias" : "Browser - Conditional OTP", + "description" : "Flow to determine if the OTP is required for the authentication", + "providerId" : "basic-flow", + "topLevel" : false, + "builtIn" : true, + "authenticationExecutions" : [ { + "authenticator" : "conditional-user-configured", + "authenticatorFlow" : false, + "requirement" : "REQUIRED", + "priority" : 10, + "autheticatorFlow" : false, + "userSetupAllowed" : false + }, { + "authenticator" : "auth-otp-form", + "authenticatorFlow" : false, + "requirement" : "REQUIRED", + "priority" : 20, + "autheticatorFlow" : false, + "userSetupAllowed" : false + } ] + }, { + "id" : "edb6e773-66f4-4c5d-96e8-72161fd4aca9", + "alias" : "Direct Grant - Conditional OTP", + "description" : "Flow to determine if the OTP is required for the authentication", + "providerId" : "basic-flow", + "topLevel" : false, + "builtIn" : true, + "authenticationExecutions" : [ { + "authenticator" : "conditional-user-configured", + "authenticatorFlow" : false, + "requirement" : "REQUIRED", + "priority" : 10, + "autheticatorFlow" : false, + "userSetupAllowed" : false + }, { + "authenticator" : "direct-grant-validate-otp", + "authenticatorFlow" : false, + "requirement" : "REQUIRED", + "priority" : 20, + "autheticatorFlow" : false, + "userSetupAllowed" : false + } ] + }, { + "id" : "22dda1be-7bc3-4bd9-9792-d54b114e7273", + "alias" : "First broker login - Conditional OTP", + "description" : "Flow to determine if the OTP is required for the authentication", + "providerId" : "basic-flow", + "topLevel" : false, + "builtIn" : true, + "authenticationExecutions" : [ { + "authenticator" : "conditional-user-configured", + "authenticatorFlow" : false, + "requirement" : "REQUIRED", + "priority" : 10, + "autheticatorFlow" : false, + "userSetupAllowed" : false + }, { + "authenticator" : "auth-otp-form", + "authenticatorFlow" : false, + "requirement" : "REQUIRED", + "priority" : 20, + "autheticatorFlow" : false, + "userSetupAllowed" : false + } ] + }, { + "id" : "cbf77531-658d-4b21-bce9-341e21357133", + "alias" : "Handle Existing Account", + "description" : "Handle what to do if there is existing account with same email/username like authenticated identity provider", + "providerId" : "basic-flow", + "topLevel" : false, + "builtIn" : true, + "authenticationExecutions" : [ { + "authenticator" : "idp-confirm-link", + "authenticatorFlow" : false, + "requirement" : "REQUIRED", + "priority" : 10, + "autheticatorFlow" : false, + "userSetupAllowed" : false + }, { + "authenticatorFlow" : true, + "requirement" : "REQUIRED", + "priority" : 20, + "autheticatorFlow" : true, + "flowAlias" : "Account verification options", + "userSetupAllowed" : false + } ] + }, { + "id" : "5df209de-e792-43dd-8fd8-ca9462d5c3d7", + "alias" : "Reset - Conditional OTP", + "description" : "Flow to determine if the OTP should be reset or not. Set to REQUIRED to force.", + "providerId" : "basic-flow", + "topLevel" : false, + "builtIn" : true, + "authenticationExecutions" : [ { + "authenticator" : "conditional-user-configured", + "authenticatorFlow" : false, + "requirement" : "REQUIRED", + "priority" : 10, + "autheticatorFlow" : false, + "userSetupAllowed" : false + }, { + "authenticator" : "reset-otp", + "authenticatorFlow" : false, + "requirement" : "REQUIRED", + "priority" : 20, + "autheticatorFlow" : false, + "userSetupAllowed" : false + } ] + }, { + "id" : "26fccf30-44b5-43a6-8bea-621d74ad48c4", + "alias" : "User creation or linking", + "description" : "Flow for the existing/non-existing user alternatives", + "providerId" : "basic-flow", + "topLevel" : false, + "builtIn" : true, + "authenticationExecutions" : [ { + "authenticatorConfig" : "create unique user config", + "authenticator" : "idp-create-user-if-unique", + "authenticatorFlow" : false, + "requirement" : "ALTERNATIVE", + "priority" : 10, + "autheticatorFlow" : false, + "userSetupAllowed" : false + }, { + "authenticatorFlow" : true, + "requirement" : "ALTERNATIVE", + "priority" : 20, + "autheticatorFlow" : true, + "flowAlias" : "Handle Existing Account", + "userSetupAllowed" : false + } ] + }, { + "id" : "a77263c0-036d-4b0f-a749-adf9bc01e32a", + "alias" : "Verify Existing Account by Re-authentication", + "description" : "Reauthentication of existing account", + "providerId" : "basic-flow", + "topLevel" : false, + "builtIn" : true, + "authenticationExecutions" : [ { + "authenticator" : "idp-username-password-form", + "authenticatorFlow" : false, + "requirement" : "REQUIRED", + "priority" : 10, + "autheticatorFlow" : false, + "userSetupAllowed" : false + }, { + "authenticatorFlow" : true, + "requirement" : "CONDITIONAL", + "priority" : 20, + "autheticatorFlow" : true, + "flowAlias" : "First broker login - Conditional OTP", + "userSetupAllowed" : false + } ] + }, { + "id" : "e62b015b-62cf-44e9-aaef-b782278d1f94", + "alias" : "browser", + "description" : "browser based authentication", + "providerId" : "basic-flow", + "topLevel" : true, + "builtIn" : true, + "authenticationExecutions" : [ { + "authenticator" : "auth-cookie", + "authenticatorFlow" : false, + "requirement" : "ALTERNATIVE", + "priority" : 10, + "autheticatorFlow" : false, + "userSetupAllowed" : false + }, { + "authenticator" : "auth-spnego", + "authenticatorFlow" : false, + "requirement" : "DISABLED", + "priority" : 20, + "autheticatorFlow" : false, + "userSetupAllowed" : false + }, { + "authenticator" : "identity-provider-redirector", + "authenticatorFlow" : false, + "requirement" : "ALTERNATIVE", + "priority" : 25, + "autheticatorFlow" : false, + "userSetupAllowed" : false + }, { + "authenticatorFlow" : true, + "requirement" : "ALTERNATIVE", + "priority" : 30, + "autheticatorFlow" : true, + "flowAlias" : "forms", + "userSetupAllowed" : false + } ] + }, { + "id" : "d9f9ab6a-a6ae-4f8f-a411-8dd0a9a11a46", + "alias" : "clients", + "description" : "Base authentication for clients", + "providerId" : "client-flow", + "topLevel" : true, + "builtIn" : true, + "authenticationExecutions" : [ { + "authenticator" : "client-secret", + "authenticatorFlow" : false, + "requirement" : "ALTERNATIVE", + "priority" : 10, + "autheticatorFlow" : false, + "userSetupAllowed" : false + }, { + "authenticator" : "client-jwt", + "authenticatorFlow" : false, + "requirement" : "ALTERNATIVE", + "priority" : 20, + "autheticatorFlow" : false, + "userSetupAllowed" : false + }, { + "authenticator" : "client-secret-jwt", + "authenticatorFlow" : false, + "requirement" : "ALTERNATIVE", + "priority" : 30, + "autheticatorFlow" : false, + "userSetupAllowed" : false + }, { + "authenticator" : "client-x509", + "authenticatorFlow" : false, + "requirement" : "ALTERNATIVE", + "priority" : 40, + "autheticatorFlow" : false, + "userSetupAllowed" : false + } ] + }, { + "id" : "b63e66d7-e30d-4458-b56c-e3d10c48ad9a", + "alias" : "direct grant", + "description" : "OpenID Connect Resource Owner Grant", + "providerId" : "basic-flow", + "topLevel" : true, + "builtIn" : true, + "authenticationExecutions" : [ { + "authenticator" : "direct-grant-validate-username", + "authenticatorFlow" : false, + "requirement" : "REQUIRED", + "priority" : 10, + "autheticatorFlow" : false, + "userSetupAllowed" : false + }, { + "authenticator" : "direct-grant-validate-password", + "authenticatorFlow" : false, + "requirement" : "REQUIRED", + "priority" : 20, + "autheticatorFlow" : false, + "userSetupAllowed" : false + }, { + "authenticatorFlow" : true, + "requirement" : "CONDITIONAL", + "priority" : 30, + "autheticatorFlow" : true, + "flowAlias" : "Direct Grant - Conditional OTP", + "userSetupAllowed" : false + } ] + }, { + "id" : "20dd3a17-943d-40a4-a320-b78a018c7740", + "alias" : "docker auth", + "description" : "Used by Docker clients to authenticate against the IDP", + "providerId" : "basic-flow", + "topLevel" : true, + "builtIn" : true, + "authenticationExecutions" : [ { + "authenticator" : "docker-http-basic-authenticator", + "authenticatorFlow" : false, + "requirement" : "REQUIRED", + "priority" : 10, + "autheticatorFlow" : false, + "userSetupAllowed" : false + } ] + }, { + "id" : "8dc9f372-c43c-48c3-80a4-f7fccb183a10", + "alias" : "first broker login", + "description" : "Actions taken after first broker login with identity provider account, which is not yet linked to any Keycloak account", + "providerId" : "basic-flow", + "topLevel" : true, + "builtIn" : true, + "authenticationExecutions" : [ { + "authenticatorConfig" : "review profile config", + "authenticator" : "idp-review-profile", + "authenticatorFlow" : false, + "requirement" : "REQUIRED", + "priority" : 10, + "autheticatorFlow" : false, + "userSetupAllowed" : false + }, { + "authenticatorFlow" : true, + "requirement" : "REQUIRED", + "priority" : 20, + "autheticatorFlow" : true, + "flowAlias" : "User creation or linking", + "userSetupAllowed" : false + } ] + }, { + "id" : "2a615cce-f472-44dc-a379-c7b28ac78e2b", + "alias" : "forms", + "description" : "Username, password, otp and other auth forms.", + "providerId" : "basic-flow", + "topLevel" : false, + "builtIn" : true, + "authenticationExecutions" : [ { + "authenticator" : "auth-username-password-form", + "authenticatorFlow" : false, + "requirement" : "REQUIRED", + "priority" : 10, + "autheticatorFlow" : false, + "userSetupAllowed" : false + }, { + "authenticatorFlow" : true, + "requirement" : "CONDITIONAL", + "priority" : 20, + "autheticatorFlow" : true, + "flowAlias" : "Browser - Conditional OTP", + "userSetupAllowed" : false + } ] + }, { + "id" : "e929e547-123a-4d1d-9d53-da597fda78ce", + "alias" : "registration", + "description" : "registration flow", + "providerId" : "basic-flow", + "topLevel" : true, + "builtIn" : true, + "authenticationExecutions" : [ { + "authenticator" : "registration-page-form", + "authenticatorFlow" : true, + "requirement" : "REQUIRED", + "priority" : 10, + "autheticatorFlow" : true, + "flowAlias" : "registration form", + "userSetupAllowed" : false + } ] + }, { + "id" : "f2166939-6982-468f-9be9-0217f35c2386", + "alias" : "registration form", + "description" : "registration form", + "providerId" : "form-flow", + "topLevel" : false, + "builtIn" : true, + "authenticationExecutions" : [ { + "authenticator" : "registration-user-creation", + "authenticatorFlow" : false, + "requirement" : "REQUIRED", + "priority" : 20, + "autheticatorFlow" : false, + "userSetupAllowed" : false + }, { + "authenticator" : "registration-profile-action", + "authenticatorFlow" : false, + "requirement" : "REQUIRED", + "priority" : 40, + "autheticatorFlow" : false, + "userSetupAllowed" : false + }, { + "authenticator" : "registration-password-action", + "authenticatorFlow" : false, + "requirement" : "REQUIRED", + "priority" : 50, + "autheticatorFlow" : false, + "userSetupAllowed" : false + }, { + "authenticator" : "registration-recaptcha-action", + "authenticatorFlow" : false, + "requirement" : "DISABLED", + "priority" : 60, + "autheticatorFlow" : false, + "userSetupAllowed" : false + } ] + }, { + "id" : "6db0cf80-5f81-4bcf-8e0b-3c4a682995eb", + "alias" : "reset credentials", + "description" : "Reset credentials for a user if they forgot their password or something", + "providerId" : "basic-flow", + "topLevel" : true, + "builtIn" : true, + "authenticationExecutions" : [ { + "authenticator" : "reset-credentials-choose-user", + "authenticatorFlow" : false, + "requirement" : "REQUIRED", + "priority" : 10, + "autheticatorFlow" : false, + "userSetupAllowed" : false + }, { + "authenticator" : "reset-credential-email", + "authenticatorFlow" : false, + "requirement" : "REQUIRED", + "priority" : 20, + "autheticatorFlow" : false, + "userSetupAllowed" : false + }, { + "authenticator" : "reset-password", + "authenticatorFlow" : false, + "requirement" : "REQUIRED", + "priority" : 30, + "autheticatorFlow" : false, + "userSetupAllowed" : false + }, { + "authenticatorFlow" : true, + "requirement" : "CONDITIONAL", + "priority" : 40, + "autheticatorFlow" : true, + "flowAlias" : "Reset - Conditional OTP", + "userSetupAllowed" : false + } ] + }, { + "id" : "8afa88d2-dbd0-4e51-bf01-3032c1221d34", + "alias" : "saml ecp", + "description" : "SAML ECP Profile Authentication Flow", + "providerId" : "basic-flow", + "topLevel" : true, + "builtIn" : true, + "authenticationExecutions" : [ { + "authenticator" : "http-basic-authenticator", + "authenticatorFlow" : false, + "requirement" : "REQUIRED", + "priority" : 10, + "autheticatorFlow" : false, + "userSetupAllowed" : false + } ] + } ], + "authenticatorConfig" : [ { + "id" : "629cf0f7-f153-4f85-9bfe-2f5c78491119", + "alias" : "create unique user config", + "config" : { + "require.password.update.after.registration" : "false" + } + }, { + "id" : "2ee0fe91-8964-4c02-982a-f3640f0fa9de", + "alias" : "review profile config", + "config" : { + "update.profile.on.first.login" : "missing" + } + } ], + "requiredActions" : [ { + "alias" : "CONFIGURE_TOTP", + "name" : "Configure OTP", + "providerId" : "CONFIGURE_TOTP", + "enabled" : true, + "defaultAction" : false, + "priority" : 10, + "config" : { } + }, { + "alias" : "TERMS_AND_CONDITIONS", + "name" : "Terms and Conditions", + "providerId" : "TERMS_AND_CONDITIONS", + "enabled" : false, + "defaultAction" : false, + "priority" : 20, + "config" : { } + }, { + "alias" : "UPDATE_PASSWORD", + "name" : "Update Password", + "providerId" : "UPDATE_PASSWORD", + "enabled" : true, + "defaultAction" : false, + "priority" : 30, + "config" : { } + }, { + "alias" : "UPDATE_PROFILE", + "name" : "Update Profile", + "providerId" : "UPDATE_PROFILE", + "enabled" : true, + "defaultAction" : false, + "priority" : 40, + "config" : { } + }, { + "alias" : "VERIFY_EMAIL", + "name" : "Verify Email", + "providerId" : "VERIFY_EMAIL", + "enabled" : true, + "defaultAction" : false, + "priority" : 50, + "config" : { } + }, { + "alias" : "delete_account", + "name" : "Delete Account", + "providerId" : "delete_account", + "enabled" : false, + "defaultAction" : false, + "priority" : 60, + "config" : { } + }, { + "alias" : "webauthn-register", + "name" : "Webauthn Register", + "providerId" : "webauthn-register", + "enabled" : true, + "defaultAction" : false, + "priority" : 70, + "config" : { } + }, { + "alias" : "webauthn-register-passwordless", + "name" : "Webauthn Register Passwordless", + "providerId" : "webauthn-register-passwordless", + "enabled" : true, + "defaultAction" : false, + "priority" : 80, + "config" : { } + }, { + "alias" : "update_user_locale", + "name" : "Update User Locale", + "providerId" : "update_user_locale", + "enabled" : true, + "defaultAction" : false, + "priority" : 1000, + "config" : { } + } ], + "browserFlow" : "browser", + "registrationFlow" : "registration", + "directGrantFlow" : "direct grant", + "resetCredentialsFlow" : "reset credentials", + "clientAuthenticationFlow" : "clients", + "dockerAuthenticationFlow" : "docker auth", + "attributes" : { + "cibaBackchannelTokenDeliveryMode" : "poll", + "cibaExpiresIn" : "120", + "cibaAuthRequestedUserHint" : "login_hint", + "oauth2DeviceCodeLifespan" : "600", + "clientOfflineSessionMaxLifespan" : "0", + "oauth2DevicePollingInterval" : "5", + "clientSessionIdleTimeout" : "0", + "parRequestUriLifespan" : "60", + "clientSessionMaxLifespan" : "0", + "clientOfflineSessionIdleTimeout" : "0", + "cibaInterval" : "5", + "realmReusableOtpCode" : "false" + }, + "keycloakVersion" : "22.0.1", + "userManagedAccessAllowed" : false, + "clientProfiles" : { + "profiles" : [ ] + }, + "clientPolicies" : { + "policies" : [ ] + } +} diff --git a/deployments/aws/templates/combined-services-config.yaml.template b/deployments/aws/templates/combined-services-config.yaml.template new file mode 100644 index 00000000..bc10d034 --- /dev/null +++ b/deployments/aws/templates/combined-services-config.yaml.template @@ -0,0 +1,57 @@ +logging: + level: info # valid levels: error, warning, info, debug + output-paths: + - stdout + - /opt/veraison/logs/{{ .service }}-stdout.log +provisioning: + listen-addr: 0.0.0.0:${PROVISIONING_PORT} + protocol: https + cert: /opt/veraison/certs/provisioning.crt + cert-key: /opt/veraison/certs/provisioning.key +verification: + listen-addr: 0.0.0.0:${VERIFICATION_PORT} + protocol: https + cert: /opt/veraison/certs/verification.crt + cert-key: /opt/veraison/certs/verification.key +management: + listen-addr: 0.0.0.0:${MANAGEMENT_PORT} + protocol: https + cert: /opt/veraison/certs/management.crt + cert-key: /opt/veraison/certs/management.key +vts: + server-addr: localhost:${VTS_PORT} + tls: true + cert: /opt/veraison/certs/vts.crt + cert-key: /opt/veraison/certs/vts.key + ca-certs: /opt/veraison/certs/rootCA.crt +ear-signer: + alg: ES256 + key: /opt/veraison/signing/skey.jwk +plugin: + backend: go-plugin + go-plugin: + dir: /opt/veraison/plugins/ +ta-store: + backend: sql + sql: + driver: sqlite3 + datasource: /opt/veraison/stores/ta-store.sql +en-store: + backend: sql + sql: + driver: sqlite3 + datasource: /opt/veraison/stores/en-store.sql +po-store: + backend: sql + sql: + driver: sqlite3 + datasource: /opt/veraison/stores/po-store.sql +po-agent: + backend: opa +auth: + backend: keycloak + host: ${KEYCLOAK_HOST} + port: ${KEYCLOAK_PORT} + ca-cert: /opt/veraison/certs/rootCA.crt + +# vim: set ft=yaml: diff --git a/deployments/aws/templates/image-combined.pkr.hcl b/deployments/aws/templates/image-combined.pkr.hcl index 94c63eec..c158be03 100644 --- a/deployments/aws/templates/image-combined.pkr.hcl +++ b/deployments/aws/templates/image-combined.pkr.hcl @@ -61,6 +61,11 @@ source "amazon-ebs" "ubuntu" { owners = ["099720109477"] # amazon most_recent = true } + security_group_filter { + filters = { + "tag:Class": "packer" + } + } ssh_username = "ubuntu" } diff --git a/deployments/aws/templates/image-keycloak.pkr.hcl b/deployments/aws/templates/image-keycloak.pkr.hcl new file mode 100644 index 00000000..7cb69a93 --- /dev/null +++ b/deployments/aws/templates/image-keycloak.pkr.hcl @@ -0,0 +1,121 @@ +packer { + required_plugins { + amazon = { + version = ">= 1.2.8" + source = "github.com/hashicorp/amazon" + } + } +} + +variable "deployment_name" { + type = string +} + +variable "ami_name" { + type = string +} + +variable "vpc_id" { + type = string +} + +variable "region" { + type = string + default = "eu-west-1" +} + +variable "instance_type" { + type = string + default = "t2.micro" +} + +variable "subnet_id" { + type = string +} + +variable "keycloak_version" { + type = string + default = "25.0.5" +} + +variable "conf_path" { + type = string +} + +variable "service_path" { + type = string +} + +locals { + conf_dest = "/opt/keycloak/conf/keycloak.conf" +} + +source "amazon-ebs" "ubuntu" { + ami_name = "${var.ami_name}" + instance_type = "${var.instance_type}" + region = "${var.region}" + vpc_id = "${var.vpc_id}" + subnet_id = "${var.subnet_id}" + associate_public_ip_address = true + tags = { + veraison-deployment = "${var.deployment_name}" + } + source_ami_filter { + filters = { + name = "ubuntu/images/*ubuntu-jammy-22.04-amd64-server-*" + root-device-type = "ebs" + virtualization-type = "hvm" + architecture = "x86_64" + } + owners = ["099720109477"] # amazon + most_recent = true + } + security_group_filter { + filters = { + "tag:Class": "packer" + } + } + ssh_username = "ubuntu" +} + +build { + name = "veraison-keycloak" + sources = [ + "source.amazon-ebs.ubuntu" + ] + + provisioner "file" { + source = "${var.conf_path}" + destination = "keycloak.conf" + } + + provisioner "file" { + source = "${var.service_path}" + destination = "keycloak.service" + } + + provisioner "shell" { + inline = [ + "sudo apt-get update", + "sudo apt-get update", # doing it twice as once doesn't seem to be enough .... + "sudo apt-get install -f --yes openjdk-21-jdk 2>&1", + + "sudo groupadd --system keycloak", + "sudo useradd --system --gid keycloak --no-create-home --shell /bin/false keycloak", + + "wget https://github.com/keycloak/keycloak/releases/download/${var.keycloak_version}/keycloak-${var.keycloak_version}.tar.gz", + "tar xf keycloak-${var.keycloak_version}.tar.gz", + "rm keycloak-${var.keycloak_version}.tar.gz", + "sudo mv keycloak-${var.keycloak_version} /opt/keycloak", + "sudo mv keycloak.conf /opt/keycloak/conf/keycloak.conf", + "sudo mv keycloak.service /opt/keycloak", + "sudo mkdir -p /opt/keycloak/data/import", + "sudo mkdir -p /opt/keycloak/certs", + + "sudo chown -R keycloak:keycloak /opt/keycloak", + "sudo systemctl enable /opt/keycloak/keycloak.service", + ] + } +} + +# vim: set et sts=2 sw=2: diff --git a/deployments/aws/templates/keycloak.conf.template b/deployments/aws/templates/keycloak.conf.template new file mode 100644 index 00000000..bcadc72c --- /dev/null +++ b/deployments/aws/templates/keycloak.conf.template @@ -0,0 +1,7 @@ +# See https://www.keycloak.org/server/all-config for all alvailable configuration. +http-enabled=false +hostname-strict=false +https-port=${KEYCLOAK_PORT} +https-certificate-file=/opt/keycloak/certs/keycloak.crt +https-certificate-key-file=/opt/keycloak/certs/keycloak.key + diff --git a/deployments/aws/templates/keycloak.service.template b/deployments/aws/templates/keycloak.service.template new file mode 100644 index 00000000..e6d62ae1 --- /dev/null +++ b/deployments/aws/templates/keycloak.service.template @@ -0,0 +1,16 @@ +[Unit] +Description=Keycloak Authentication server +After=network.target + +[Service] +Type=exec +Environment="KEYCLOAK_ADMIN=${KEYCLOAK_ADMIN}" "KEYCLOAK_ADMIN_PASSWORD=${KEYCLOAK_ADMIN_PASSWORD}" +WorkingDirectory=/opt/keycloak +ExecStart=/opt/keycloak/bin/kc.sh start --optimized --import-realm --verbose +Restart=always +User=keycloak + +[Install] +WantedBy=default.target + +# vim: set ft=systemd: diff --git a/deployments/aws/templates/stack-combined.yaml b/deployments/aws/templates/stack-combined.yaml index 0e625d55..d78774c7 100644 --- a/deployments/aws/templates/stack-combined.yaml +++ b/deployments/aws/templates/stack-combined.yaml @@ -16,7 +16,10 @@ Parameters: Description: | The name of an EC2 key pair that will be used to provide access to the instance. - InstanceImage: + CombinedImage: + Type: String + Description: ID of the AMI image to be used for the instance. + KeycloakImage: Type: String Description: ID of the AMI image to be used for the instance. AdminCidr: @@ -25,6 +28,9 @@ Parameters: SubnetId: Type: String Description: ID of the subnet to be used for veraison deployment + SubnetCidr: + Type: String + Description: CIDR of the subnet identified by SubnetId # optional parameters (have a default if are not specfied): ProvisioningPort: @@ -39,16 +45,19 @@ Parameters: Type: Number Description: TCP port on which the management service is listening Default: 8088 + KeycloakPort: + Type: Number + Description: TCP port on which the management service is listening + Default: 11111 ServiceInstanceType: Description: An EC2 instance type that will be used to run EC2 Instances Type: String Default: t2.micro - # TODO(setrofm): technicall, there is a set of AllowedValues that should be - # specified here (i.e. a valid type name), but since there is a ton of - # them, and right now I'm not sure which subset of those would even make - # sense for service instance, I'm leaving this unconstrained for now. + KeycloakInstanceType: + Description: An EC2 instance type that will be used to run EC2 Instances + Type: String + Default: t2.micro - Resources: VeraisonSecurityGroup: @@ -78,18 +87,58 @@ Resources: - Key: veraison-deployment Value: !Ref DeploymentName + KeycloakSecurityGroup: + Type: AWS::EC2::SecurityGroup + Properties: + VpcId: !Ref VpcId + GroupName: keycloak + GroupDescription: Keycloak access + SecurityGroupIngress: + - IpProtocol: tcp + FromPort: 22 + ToPort: 22 + CidrIp: !Ref AdminCidr + - IpProtocol: tcp + FromPort: !Ref KeycloakPort + ToPort: !Ref KeycloakPort + CidrIp: !Ref AdminCidr + - IpProtocol: tcp + FromPort: !Ref KeycloakPort + ToPort: !Ref KeycloakPort + CidrIp: !Ref SubnetCidr + Tags: + - Key: veraison-deployment + Value: !Ref DeploymentName + VeraisonInstance: Type: AWS::EC2::Instance Properties: KeyName: !Ref KeyName InstanceType: !Ref ServiceInstanceType - ImageId: !Ref InstanceImage + ImageId: !Ref CombinedImage SubnetId: !Ref SubnetId SecurityGroupIds: - !GetAtt VeraisonSecurityGroup.GroupId Tags: - Key: veraison-deployment Value: !Ref DeploymentName + - Key: deployment-instance-name + Value: combined + + KeycloakInstance: + Type: AWS::EC2::Instance + Properties: + KeyName: !Ref KeyName + InstanceType: !Ref ServiceInstanceType + ImageId: !Ref KeycloakImage + SubnetId: !Ref SubnetId + SecurityGroupIds: + - !GetAtt KeycloakSecurityGroup.GroupId + Tags: + - Key: veraison-deployment + Value: !Ref DeploymentName + - Key: deployment-instance-name + Value: keycloak VeraisonIpAddress: Type: AWS::EC2::EIP @@ -100,3 +149,13 @@ Resources: Tags: - Key: veraison-deployment Value: !Ref DeploymentName + + KeycloakIpAddress: + Type: AWS::EC2::EIP + DependsOn: VeraisonInstance + Properties: + Domain: vpc + InstanceId: !Ref KeycloakInstance + Tags: + - Key: veraison-deployment + Value: !Ref DeploymentName diff --git a/deployments/aws/templates/stack-debug.yaml b/deployments/aws/templates/stack-debug.yaml new file mode 100644 index 00000000..0cf3dfad --- /dev/null +++ b/deployments/aws/templates/stack-debug.yaml @@ -0,0 +1,76 @@ +# Veraison stack +AWSTemplateFormatVersion: 2010-09-09 +Description: Veraison attestation verfication services + +Parameters: + # mandatory parameters (no defaults): + DeploymentName: + Type: String + Description: | + The name of this deployment. + VpcId: + Description: ID for the VPC into which Veraison will be deployed + Type: AWS::EC2::VPC::Id + KeyName: + Type: AWS::EC2::KeyPair::KeyName + Description: | + The name of an EC2 key pair that will be used to provide access to the + instance. + InstanceImage: + Type: String + Description: ID of the AMI image to be used for the instance. + AdminCidr: + Type: String + Description: CIDR to used to configure remote access + SubnetId: + Type: String + Description: ID of the subnet to be used for veraison deployment + + # optional parameters (have a default if are not specfied): + InstanceType: + Description: An EC2 instance type that will be used to run EC2 Instances + Type: String + Default: t2.micro + + +Resources: + + SecurityGroup: + Type: AWS::EC2::SecurityGroup + Properties: + VpcId: !Ref VpcId + GroupName: veraison-test + GroupDescription: Veraison services access + SecurityGroupIngress: + - IpProtocol: tcp + FromPort: 22 + ToPort: 22 + CidrIp: !Ref AdminCidr + Tags: + - Key: veraison-deployment + Value: !Ref DeploymentName + + Instance: + Type: AWS::EC2::Instance + Properties: + KeyName: !Ref KeyName + InstanceType: !Ref InstanceType + ImageId: !Ref InstanceImage + SubnetId: !Ref SubnetId + SecurityGroupIds: + - !GetAtt SecurityGroup.GroupId + Tags: + - Key: veraison-deployment + Value: !Ref DeploymentName + - Key: deployment-instance-name + Value: debug + + IpAddress: + Type: AWS::EC2::EIP + DependsOn: Instance + Properties: + Domain: vpc + InstanceId: !Ref Instance + Tags: + - Key: veraison-deployment + Value: !Ref DeploymentName diff --git a/deployments/debian/debian/postinst b/deployments/debian/debian/postinst index 83c5bba0..fdaa232a 100644 --- a/deployments/debian/debian/postinst +++ b/deployments/debian/debian/postinst @@ -22,5 +22,7 @@ if [ "$1" = "configure" ]; then chown -R "$VERAISON_USER":"$VERAISON_GROUP" /opt/veraison/certs chown -R "$VERAISON_USER":"$VERAISON_GROUP" /opt/veraison/stores + chmod 0500 /opt/veraison/certs/*.key + /opt/veraison/bin/veraison -s start-services fi diff --git a/end-to-end/end-to-end-aws b/end-to-end/end-to-end-aws index bd4557a7..9475838e 100755 --- a/end-to-end/end-to-end-aws +++ b/end-to-end/end-to-end-aws @@ -9,9 +9,8 @@ THIS_DIR=$( cd -- "$( dirname -- "${BASH_SOURCE[0]}" )" &> /dev/null && pwd ) source "${THIS_DIR}/../deployments/aws/env/env.bash" -if [[ ! -d "$CONFIG_DIR" ]]; then - veraison --deployment-name "${VERAISON_AWS_DEPLOYMENT}" create-client-config --output-dir "$CONFIG_DIR" -fi +veraison --quiet --deployment-name "${VERAISON_AWS_DEPLOYMENT}" \ + create-client-config --output-dir "$CONFIG_DIR" function provision() { case $SCHEME in @@ -117,22 +116,22 @@ function _check_scheme() { } function _check_installed() { - local what=$1 + local what=$1 - if [[ "$(type -p "$what")" == "" ]]; then - echo -e "$_error: $what executable must be installed to use this command." - exit 1 - fi + if [[ "$(type -p "$what")" == "" ]]; then + echo -e "$_error: $what executable must be installed to use this command." + exit 1 + fi } _error='\e[0;31mERROR\e[0m' while getopts "hs:" opt; do - case "$opt" in - h) help; exit 0;; - s) SCHEME="$OPTARG";; - *) break;; - esac + case "$opt" in + h) help; exit 0;; + s) SCHEME="$OPTARG";; + *) break;; + esac done shift $((OPTIND-1)) @@ -150,4 +149,4 @@ case $command in verify) verify "$2";; *) echo "${_error}: unexpected command: \"$command\""; help;; esac -# vim: set et sts=4 sw=4 +# vim: set et sts=4 sw=4: