def approve_cert(self, user_name, admin=False):
     """ Approve the CSR and return a cert that can be used """
     # TODO: exception handling
     user = user_name if admin else "tool-{}".format(user_name)
     body = self.certs.read_certificate_signing_request_status(user)
     # create an approval condition
     approval_condition = client.V1beta1CertificateSigningRequestCondition(
         last_update_time=datetime.now(timezone.utc).astimezone(),
         message="This certificate was approved by maintain_kubeusers",
         reason="Authorized User",
         type="Approved",
     )
     # patch the existing `body` with the new conditions
     # you might want to append the new conditions to the existing ones
     body.status.conditions = [approval_condition]
     # Patch the Kubernetes CSR object in the certs API
     # The method called to the API is very confusingly named
     _ = self.certs.replace_certificate_signing_request_approval(user, body)
     # There is a small delay in filling the certificate field, it seems.
     time.sleep(1)
     api_response = self.certs.read_certificate_signing_request(user)
     if api_response.status.certificate is not None:
         # Get the actual cert
         cert = base64.b64decode(api_response.status.certificate)
         # Clean up the API
         self.certs.delete_certificate_signing_request(
             user, body=client.V1DeleteOptions())
         return cert
     else:
         logging.error("Certificate creation stalled or failed for %s",
                       user)
Exemplo n.º 2
0
def create_approval_patch(csr: k8s.V1beta1CertificateSigningRequest,
                          date: datetime) -> None:
    # Approving CSRs works by appending a condition of type
    # "Approved" to the status.
    message = f'This CSR for node {csr.metadata.name} was approved by openshift-csr-approver'  # noqa E501
    condition = k8s.V1beta1CertificateSigningRequestCondition(
        type='Approved',
        reason='openshift-csr-approver',
        message=message,
        # Ugly "+ Z" hack to make the kubernetes API accept the UTC timestamp
        last_update_time=date.isoformat(timespec='seconds') + 'Z'
    )
    if csr.status.conditions is None:
        csr.status.conditions = []
    csr.status.conditions.append(condition)
def valiate_approve_csr(csrList, nodeInfoList):

    # Initialize empty lists
    (
        approvedBootStrappedNodes,
        approvedNodes,
        approvedNodesToLabel,
        approvedCSRs,
        approvedNodesToTaint,
    ) = ([], [], [], [], [])

    # a reference to the API
    certs_api = client.CertificatesV1beta1Api()

    # Auth user and groups
    bootstrapGroups = [
        "system:serviceaccounts:openshift-machine-config-operator",
        "system:authenticated",
        "system:serviceaccounts",
    ]
    bootstrapUser = (
        "system:serviceaccount:openshift-machine-config-operator:node-bootstrapper"
    )

    nodeGroups = ["system:nodes", "system:authenticated"]
    nodeUser = "******"

    for csrName in csrList:

        # Initialize variables
        csrType, dnsName, ip = None, None, None

        nodeUserValidation = False
        bootstrapUserValidation = False

        # obtain the body of the CSR we want to sign
        body = certs_api.read_certificate_signing_request_status(csrName)

        # Decode base64 string to pem format csr
        decodedCertificate = base64.b64decode(
            body.spec.request).decode("utf-8")

        # Load pem format csr
        try:
            certificate = cryptography.x509.load_pem_x509_csr(
                bytes(decodedCertificate, encoding="utf8"), default_backend())
        except Exception:
            print("Error loading CSR")
            sys.exit(1)

        if ("bootstrapper" in body.spec.username
                and "system:node:" not in body.spec.username):

            groupDifference = set(bootstrapGroups) - set(body.spec.groups)
            dnsName = certificate.subject.get_attributes_for_oid(
                cryptography.x509.oid.NameOID.COMMON_NAME)[0].value.split(
                    ":")[2]

            if (dnsName and body.spec.username == bootstrapUser
                    and len(groupDifference) == 0):

                csrType = "bootstrapper"
                bootstrapUserValidation = True

        elif ("bootstrapper" not in body.spec.username
              and "system:node:" in body.spec.username):

            groupDifference = set(nodeGroups) - set(body.spec.groups)

            crt_san_data = certificate.extensions.get_extension_for_oid(
                cryptography.x509.oid.ExtensionOID.SUBJECT_ALTERNATIVE_NAME)
            dnsName = crt_san_data.value.get_values_for_type(
                cryptography.x509.DNSName)[0]
            ip = crt_san_data.value.get_values_for_type(
                cryptography.x509.IPAddress)[0]

            if (dnsName and body.spec.username == nodeUser + dnsName
                    and len(groupDifference) == 0):

                csrType = "node"
                nodeUserValidation = True

        else:
            print("Unknown CSR type")
            sys.exit(1)

        if (csrType == "node" and next(
            (item for item in nodeInfoList if item["node"] == dnsName), None)
                and next(
                    (item
                     for item in nodeInfoList if item["ip"] == str(ip)), None)
                and nodeUserValidation) or (csrType == "bootstrapper" and next(
                    (item for item in nodeInfoList if item["node"] == dnsName),
                    None) and bootstrapUserValidation):

            # create an approval condition
            approval_condition = client.V1beta1CertificateSigningRequestCondition(
                last_update_time=datetime.now(timezone.utc).astimezone(),
                message=
                "This certificate was approved by CSR Approval Operator",
                reason="Validated and approved by CSR Approval Operator",
                type="Approved",
            )

            # patch the existing `body` with the new conditions
            # you might want to append the new conditions to the existing ones
            body.status.conditions = [approval_condition]

            # patch the Kubernetes object
            response = certs_api.replace_certificate_signing_request_approval(
                csrName, body)

            # Increase count of approvedCSRCount variable
            approvedCSRs.append(csrName)

            if csrType == "node" and any("label" in d for d in nodeInfoList
                                         if d["node"] == dnsName):
                approvedNodesToLabel.append({
                    "node":
                    dnsName,
                    "label":
                    "".join([
                        d["label"] for d in nodeInfoList
                        if d["node"] == dnsName
                    ]),
                })
                approvedNodes.append(dnsName)

            if csrType == "node" and any("taint" in d for d in nodeInfoList
                                         if d["node"] == dnsName):
                approvedNodesToTaint.append({
                    "node":
                    dnsName,
                    "taint":
                    "".join([
                        d["taint"] for d in nodeInfoList
                        if d["node"] == dnsName
                    ]),
                })
                approvedNodes.append(dnsName)

            if csrType == "node" and not any("label" in d for d in nodeInfoList
                                             if d["node"] == dnsName):
                approvedNodes.append(dnsName)

            if csrType == "bootstrapper":
                approvedBootStrappedNodes.append(dnsName)

            approvedNodes = list(dict.fromkeys(approvedNodes))

    return (
        approvedNodes,
        approvedCSRs,
        approvedNodesToLabel,
        approvedBootStrappedNodes,
        approvedNodesToTaint,
    )
    api_version='certificates.k8s.io/v1beta1',
    kind='CertificateSigningRequest',
    metadata=k8s.V1ObjectMeta(name='csr-approved',
                              uid='3585ad87-19d4-4729-b99a-a422cae24713'),
    spec=k8s.V1beta1CertificateSigningRequestSpec(
        groups=['system:nodes', 'system:authenticated'],
        uid='3585ad87-19d4-4729-b99a-a422cae24713',
        usages=['digital signature', 'key encipherment', 'server auth'],
        username='******',
        request=
        'LS0tLS1CRUdJTiBDRVJUSUZJQ0FURSBSRVFVRVNULS0tLS0KTUlJQlFUQ0I3QUlCQURBM01SVXdFd1lEVlFRS0RBeHplWE4wWlcwNmJtOWtaWE14SGpBY0JnTlZCQU1NRlhONQpjM1JsYlRwdWIyUmxPbTFoYzNSbGNpMHdNVEJjTUEwR0NTcUdTSWIzRFFFQkFRVUFBMHNBTUVnQ1FRQ2xjcHM5CnhyL3hFaFdEbG4xR3lRanU0WW1pQTZkaVVkdzh6LzQ1WXZPODh0L2w1bmVlZUtMd1ZpZysrRk9zeXpSZ1p4elIKTHVLT0JzRDRGU0FKZGFQdkFnTUJBQUdnVURCT0Jna3Foa2lHOXcwQkNRNHhRVEEvTUQwR0ExVWRFUUVCL3dRegpNREdDQ1cxaGMzUmxjaTB3TVlJWWJXRnpkR1Z5TFRBeExtOXpMbVY0WVcxd2JHVXVZMjl0aHdRS0tnQUJod1RBCnFDb0JNQTBHQ1NxR1NJYjNEUUVCQ3dVQUEwRUFvZFRzSWRXUExmbkR1TE9GTWJSZCtkc3Q5WjlLY3dTQ043eWMKdU14YzRXVmhvRGZpTUtOUllPcEZ6YUZPZVF2SFJ1RVJWQXY0c3BnL21LOFQrN01JN1E9PQotLS0tLUVORCBDRVJUSUZJQ0FURSBSRVFVRVNULS0tLS0K'  # noqa E501
    ),
    status=k8s.V1beta1CertificateSigningRequestStatus(conditions=[
        k8s.V1beta1CertificateSigningRequestCondition(
            last_update_time='2020-03-06T17:45:00+01:00',
            type='Approved',
            reason='Testing',
            message='Approved for testing purposes')
    ]))

CSR_DENIED = k8s.V1beta1CertificateSigningRequest(
    api_version='certificates.k8s.io/v1beta1',
    kind='CertificateSigningRequest',
    metadata=k8s.V1ObjectMeta(name='csr-denied',
                              uid='bbfebbed-c71d-4eb4-87e8-8ba05fc29198'),
    spec=k8s.V1beta1CertificateSigningRequestSpec(
        groups=['system:nodes', 'system:authenticated'],
        uid='bbfebbed-c71d-4eb4-87e8-8ba05fc29198',
        usages=['digital signature', 'key encipherment', 'server auth'],
        username='******',
        request=
Exemplo n.º 5
0
def submit_and_approve_k8s_csr(namespace, certificates_api, k8s_csr):
    """Function to submit or approve a Kubernetes CSR"""

    new_k8s_csr_name = k8s_csr.metadata.name

    # Read existing Kubernetes CSR
    try:

        logging.info("Looking for existing CSR")
        certificates_api.read_certificate_signing_request(new_k8s_csr_name)

    except ApiException as exception:

        if exception.status != 404:

            logging.error(
                f"Problem reading existing certificate requests: {exception}\n"
            )
            sys.exit(1)

        elif exception.status == 404:

            logging.info(f"Did not find existing certificate requests")
            logging.debug(f"Exception:\n{exception}\n")

    else:

        try:

            logging.info("Deleting k8s csr")

            certificates_api.delete_certificate_signing_request(
                new_k8s_csr_name)

        except ApiException as exception:

            if exception.status != 404:

                logging.error(
                    f'Unable to delete existing certificate request "{new_k8s_csr_name}": {exception}\n'
                )
                sys.exit(1)

            elif exception.status == 404:

                logging.info(
                    f'Existing certificate request "{new_k8s_csr_name}" not found'
                )
                logging.debug(f"Exception:\n{exception}\n")
        else:

            logging.info(f"Existing certificate request deleted")

    # Create K8s CSR resource
    try:

        logging.info("Create k8s CSR")
        logging.debug(k8s_csr)
        certificates_api.create_certificate_signing_request(k8s_csr)

    except ApiException as exception:

        logging.error(
            f'Unable to create certificate request "{new_k8s_csr_name}"\n')
        logging.debug(f"Exception:\n{exception}\n")
        sys.exit(1)

    logging.info(
        f'Certificate signing request "{new_k8s_csr_name}" has been created')

    # Read newly created K8s CSR resource
    try:

        new_k8s_csr_body = certificates_api.read_certificate_signing_request_status(
            new_k8s_csr_name)

    except ApiException as exception:

        logging.error(
            f'Unable to read certificate request status for "{new_k8s_csr_name}"\n'
        )
        logging.debug(f"Exception:\n{exception}\n")
        sys.exit(1)

    new_k8s_csr_approval_conditions = client.V1beta1CertificateSigningRequestCondition(
        last_update_time=datetime.datetime.now(datetime.timezone.utc),
        message=
        f"This certificate was approved by MagTape (pod: {magtape_pod_name})",
        reason="MT-Approve",
        type="Approved",
    )

    # Update the CSR status
    new_k8s_csr_body.status.conditions = [new_k8s_csr_approval_conditions]

    # Patch the k8s CSR resource
    try:

        logging.info(f"Patch k8s CSR: {new_k8s_csr_name}")
        certificates_api.replace_certificate_signing_request_approval(
            new_k8s_csr_name, new_k8s_csr_body)

    except ApiException as exception:

        logging.info(
            f'Unable to update certificate request status for "{new_k8s_csr_name}": {exception}\n'
        )

    logging.info(
        f'Certificate signing request "{new_k8s_csr_name}" is approved')

    return new_k8s_csr_body
Exemplo n.º 6
0
def build_certs():
    base_path = path.join(path.dirname(path.abspath(__file__)), "certs")
    ssl_path = path.join(path.dirname(path.dirname(path.abspath(__file__))),
                         "ssl")
    with open(path.join(base_path, "csr-conf.tpl")) as fh:
        data = fh.read()

    template = TEMPLATE_ENV.from_string(data)
    rendered_data = template.render(app_name=APP_NAME, namespace=NAMESPACE)
    with open("/tmp/csr.conf", "w") as fh:
        fh.write(rendered_data)

    logger.info("Generating RSA Key file...")
    _ = _openssl("genrsa", "-out", "/tmp/server-key.pem", "2048")

    logger.info("Generating CSR Request data...")
    _ = _openssl(
        "req",
        "-new",
        "-key",
        "/tmp/server-key.pem",
        "-subj",
        "/CN={}.{}.svc".format(APP_NAME, NAMESPACE),
        "-out",
        "/tmp/server.csr",
        "-config",
        "/tmp/csr.conf",
    )

    _cleanup_csr()

    with open("/tmp/server.csr") as fh:
        request_data = fh.read()

    with open(path.join(base_path, "csr.yaml")) as fh:
        data = fh.read()

    template = TEMPLATE_ENV.from_string(data)
    rendered_data = template.render(csr_name=CSR_NAME,
                                    csr_data=b64encode(
                                        request_data.encode()).decode())
    rendered_data = safe_load(rendered_data)

    logger.info("Creating a CSR for %s", CSR_NAME)

    cert_api.create_certificate_signing_request(body=rendered_data)

    logger.info("Approving CSR for %s", CSR_NAME)
    approval_condition = client.V1beta1CertificateSigningRequestCondition(
        last_update_time=datetime.now().astimezone(),
        message="This certificate was approved by Python Client API",
        reason="MyOwnReason",
        type="Approved",
    )
    body = cert_api.read_certificate_signing_request_status(name=CSR_NAME)
    body.status.conditions = [approval_condition]
    cert_api.replace_certificate_signing_request_approval(name=CSR_NAME,
                                                          body=body)

    for attempt in range(20):
        logger.info("[Attempt %d] Checking if the CSR for %s is approved",
                    attempt, CSR_NAME)
        csr_data = cert_api.read_certificate_signing_request(name=CSR_NAME)
        if csr_data.status.certificate:
            logger.info(csr_data.status.certificate)
            with open("/tmp/cert.data", "w") as fh:
                fh.write(csr_data.status.certificate)

            _ = _openssl(
                "base64",
                "-d",
                "-A",
                "-out",
                "/tmp/server-cert.pem",
                "-in",
                "/tmp/cert.data",
            )
            break
        else:
            logger.warning(
                "CSR for %s is not yet approved. Retrying in 5 seconds...",
                CSR_NAME)
            sleep(5)
    else:
        logger.error("Failed to generate singed certs for %s", CSR_NAME)
        exit(1)

    logger.info("Successfully completed generating Certs for %s", CSR_NAME)
    copyfile("/tmp/server-key.pem", path.join(ssl_path, "key.pem"))
    copyfile("/tmp/server-cert.pem", path.join(ssl_path, "cert.pem"))
Exemplo n.º 7
0
                                allowed = False
                                ip = clean_extension_split[1].replace(
                                    'IP Address:', '').strip()
                                for cidr in allowed_networks:
                                    if ip_address(ip) in ip_network(cidr):
                                        allowed = True
                                        break
                                if not allowed:
                                    print(
                                        "Invalid Ip %s from csr %s. Ignoring" %
                                        (ip, csr_name))
                                    continue
                for rule in name_rules:
                    if re.match(rule, cert_name):
                        print("Signing %s cert %s" % (cert_type, csr_name))
                        body = certs_api.read_certificate_signing_request_status(
                            csr_name)
                        now = datetime.now(timezone.utc).astimezone()
                        message = "Signed by Karmab autosigner"
                        reason = "Matching autosigner rules"
                        approval_condition = client.V1beta1CertificateSigningRequestCondition(
                            last_update_time=now,
                            message=message,
                            reason=reason,
                            type='Approved')
                        body.status.conditions = [approval_condition]
                        certs_api.replace_certificate_signing_request_approval(
                            csr_name, body)
                        break
                continue
Exemplo n.º 8
0
def watch_csrs():
    while True:
        stream = watch.Watch().stream(
            certs_api.list_certificate_signing_request, timeout_seconds=10)
        for event in stream:
            obj = event["object"]
            obj_dict = obj.to_dict()
            csr_name = obj_dict['metadata']['name']
            request = obj_dict['spec']['request']
            usages = obj_dict['spec'].get('usages', [])
            username = obj_dict['spec']['username']
            groups = obj_dict['spec']['groups']
            if 'client auth' in usages:
                if username != 'system:serviceaccount:openshift-machine-config-operator:node-bootstrapper':
                    print("Incorrect username in csr %s. Ignoring" % csr_name)
                    continue
                if sorted(groups) != [
                        'system:authenticated', 'system:serviceaccounts',
                        'system:serviceaccounts:openshift-machine-config-operator'
                ]:
                    print("Incorrect group in csr %s. Ignoring" % csr_name)
                    continue
            elif 'server auth' in usages:
                if sorted(groups) != ['system:authenticated', 'system:nodes']:
                    print("Incorrect group in csr %s. Ignoring" % csr_name)
                    continue
            else:
                continue
            cert_type = 'client' if 'client auth' in usages else 'server'
            status = obj_dict['status']
            if status['conditions'] is None:
                csr_pem = b64decode(request)
                csr = load_certificate_request(FILETYPE_PEM, csr_pem)
                cert_name = str(csr.get_subject())
                common_names = [
                    e[1].decode() for e in csr.get_subject().get_components()
                    if e[0].decode() == 'CN'
                ]
                if not common_names:
                    print("Invalid common name in csr %s. Ignoring" % csr_name)
                    continue
                else:
                    common_name = common_names[0]
                if 'server auth' in usages:
                    if username != common_name:
                        print("Incorrect username in csr %s. Ignoring" %
                              csr_name)
                        continue
                    for extension in csr.get_extensions():
                        clean_extension = str(extension)
                        if clean_extension.startswith(
                                'DNS') and 'IP Address:' in clean_extension:
                            clean_extension_split = clean_extension.split(',')
                            dns = clean_extension_split[0].replace('DNS:', '')
                            if 'system:node:%s' % dns != common_name:
                                print(
                                    "Incorrect DNS name in csr %s. Ignoring" %
                                    csr_name)
                                continue
                            if allowed_networks:
                                allowed = False
                                ip = clean_extension_split[1].replace(
                                    'IP Address:', '').strip()
                                for cidr in allowed_networks:
                                    if ip_address(ip) in ip_network(cidr):
                                        allowed = True
                                        break
                                if not allowed:
                                    print(
                                        "Invalid Ip %s from csr %s. Ignoring" %
                                        (ip, csr_name))
                                    continue
                for rule in name_rules:
                    common_name = common_name.replace("system:node:", "")
                    if re.match(rule, cert_name) or re.match(
                            rule, common_name):
                        print("Signing %s cert %s" % (cert_type, csr_name))
                        body = certs_api.read_certificate_signing_request_status(
                            csr_name)
                        now = datetime.now(timezone.utc).astimezone()
                        message = "Signed by Karmab autosigner"
                        reason = "Matching autosigner rules"
                        approval_condition = client.V1beta1CertificateSigningRequestCondition(
                            last_update_time=now,
                            message=message,
                            reason=reason,
                            type='Approved')
                        body.status.conditions = [approval_condition]
                        try:
                            certs_api.replace_certificate_signing_request_approval(
                                csr_name, body)
                        except Exception as e:
                            print(
                                "Hit %s when signing cert %s. This will be retried"
                                % (e, csr_name))
                        break
                continue