From 19ca30e4f43cc94e49733a91e68d8190f03af07c Mon Sep 17 00:00:00 2001 From: Annie Ku Date: Wed, 5 Feb 2025 15:00:03 -0800 Subject: [PATCH] feat: create module for configuring an okta app with a kms key (#691) * fix: create module for configuring an okta app with a kms key * fix: use right references here * retrigger gha * fix: create kms-okta-app readme to explain how to use the module * fix: update docs to reflect new parameters * fix: update okta link to a more relevant section for keys * fix: add another resource about jwt payload fields --- kms-okta-app/Makefile | 12 +++++ kms-okta-app/README.md | 85 +++++++++++++++++++++++++++++++ kms-okta-app/fogg.tf | 2 + kms-okta-app/get_jwks_for_okta.py | 28 ++++++++++ kms-okta-app/main.tf | 35 +++++++++++++ kms-okta-app/outputs.tf | 7 +++ kms-okta-app/run.sh | 5 ++ kms-okta-app/terragrunt.hcl | 4 ++ kms-okta-app/variables.tf | 48 +++++++++++++++++ kms-okta-app/versions.tf | 10 ++++ 10 files changed, 236 insertions(+) create mode 100644 kms-okta-app/Makefile create mode 100644 kms-okta-app/README.md create mode 100644 kms-okta-app/fogg.tf create mode 100644 kms-okta-app/get_jwks_for_okta.py create mode 100644 kms-okta-app/main.tf create mode 100644 kms-okta-app/outputs.tf create mode 100644 kms-okta-app/run.sh create mode 100644 kms-okta-app/terragrunt.hcl create mode 100644 kms-okta-app/variables.tf create mode 100644 kms-okta-app/versions.tf diff --git a/kms-okta-app/Makefile b/kms-okta-app/Makefile new file mode 100644 index 00000000..939b8d87 --- /dev/null +++ b/kms-okta-app/Makefile @@ -0,0 +1,12 @@ +# Auto-generated by fogg. Do not edit +# Make improvements in fogg, so that everyone can benefit. + +export TERRAFORM_VERSION := 1.3.6 +export TF_PLUGIN_CACHE_DIR := ../../..//.terraform.d/plugin-cache + +include ../../..//scripts/module.mk + + +help: ## display help for this makefile + @fgrep -h "##" $(MAKEFILE_LIST) | fgrep -v fgrep | sed -e 's/\\$$//' | sort | awk 'BEGIN {FS = ":.*?## "}; {printf "\033[36m%-30s\033[0m %s\n", $$1, $$2}' +.PHONY: help diff --git a/kms-okta-app/README.md b/kms-okta-app/README.md new file mode 100644 index 00000000..5e0a0e28 --- /dev/null +++ b/kms-okta-app/README.md @@ -0,0 +1,85 @@ +This is a module that creates an Okta app that's bounded to a KMS Key. You can use the KMS key to sign JWTs that are compatible with the Okta /token endpoints. After you create the Okta App, you can work with the Okta Token endpoint by roughly following these steps: +1. Creating a header with this kind of structure: `{{"alg": "", "typ": "JWT"}}`. To identify algorithms. Here are the algorithm options as of Febuary 5, 2025: +``` +signing_algs = ( + ('RSASSA_PSS_SHA_256', 'PS256', 'sha256'), + ('RSASSA_PSS_SHA_384', 'PS384', 'sha384'), + ('RSASSA_PSS_SHA_512', 'PS512', 'sha512'), + ('RSASSA_PKCS1_V1_5_SHA_256', 'RS256', 'sha256'), + ('RSASSA_PKCS1_V1_5_SHA_384', 'RS384', 'sha384'), + ('RSASSA_PKCS1_V1_5_SHA_512', 'RS512', 'sha512'), + ('ECDSA_SHA_256', 'ES256', 'sha256'), + ('ECDSA_SHA_384', 'ES384', 'sha384'), + ('ECDSA_SHA_512', 'ES512', 'sha512'), + ) +``` +source: https://github.com/jmtapio/python-jwt-kms/blob/552668588c5eec5eff9346740660239baef22428/jwt_kms/jwa.py +So if your KMS key has type `RSASSA_PKCS1_V1_5_SHA_256`, your `alg` is `RS256`. If you want to dive deeper, you can look at the RFC section here: https://datatracker.ietf.org/doc/html/rfc7518#section-3.1 + +2. Creating a payload with this kind of structure with this format: +```json +{ + "exp": , + "iat": , + "iss": client_id, + "aud": [okta_token_endpoint], + "sub": client_id, +} +``` +each attribute can be found here: https://developer.okta.com/docs/api/openapi/okta-oauth/guides/client-auth/#token-claims-for-client-authentication-with-client-secret-or-private-key-jwt + +3. Base64-encode values from step #1 and #2 and strip out the equal signs (`=`). +4. Concatenate the values from step #3 with a header.payload format, then use the AWS KMS `sign` operation from whatever AWS SDK you use. If you used algorithm type `RS256`, then your KMS Sign operation should use SigningAlgorithm type `RSASSA_PKCS1_V1_5_SHA_256`. You should be able to parse out the "Signature" value from your kms `sign` output. +5. Construct the JWT in this way: base64header.base64payload.signature-from-step-4 + +After you construct the JWT, you're ready to use it with the Okta Token endpoint here: https://developer.okta.com/docs/api/openapi/okta-oauth/guides/client-auth/#jwt-with-private-key +you just need to set the JWT from step 5 to the "client_assertion" value while making the request. + + +## Requirements + +| Name | Version | +|------|---------| +| [terraform](#requirement\_terraform) | >= 1.3 | +| [okta](#requirement\_okta) | ~> 4.0 | + +## Providers + +| Name | Version | +|------|---------| +| [aws](#provider\_aws) | n/a | +| [external](#provider\_external) | n/a | +| [okta](#provider\_okta) | ~> 4.0 | + +## Modules + +| Name | Source | Version | +|------|--------|---------| +| [params](#module\_params) | github.com/chanzuckerberg/cztack//aws-ssm-params-writer | v0.63.3 | + +## Resources + +| Name | Type | +|------|------| +| [okta_app_oauth.idp_api](https://registry.terraform.io/providers/okta/okta/latest/docs/resources/app_oauth) | resource | +| [aws_caller_identity.current](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/caller_identity) | data source | +| [aws_region.current](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/region) | data source | +| [external_external.jwks_info](https://registry.terraform.io/providers/hashicorp/external/latest/docs/data-sources/external) | data source | + +## Inputs + +| Name | Description | Type | Default | Required | +|------|-------------|------|---------|:--------:| +| [friendly\_key\_identifier](#input\_friendly\_key\_identifier) | A name for the key configuration in the okta app, something you will recognize for yourself and the project. | `string` | n/a | yes | +| [kms\_key\_id](#input\_kms\_key\_id) | The Key ID or alias of the AWS KMS Key. It has to be available in the same region and account as the configured provider. | `string` | n/a | yes | +| [okta\_configuration](#input\_okta\_configuration) | Details needed to configure an okta app. Its token auth method is private\_key\_jwt |
object({
label = string
type = string
grant_types = list(string)
omit_secret = bool
response_types = list(string)
pkce_required = bool
})
| n/a | yes | +| [tags](#input\_tags) | These values are used to derive the path in the param store where to write the Okta App Configuration metadata. |
object({
project = string,
env = string,
service = string,
owner = string,
})
| n/a | yes | +| [write\_metadata\_to\_params](#input\_write\_metadata\_to\_params) | Whether you want to include the clientID and KMS Key Alias grouped together as securestring parameters in JSON format. If true, module will write these details to a path based on the env, project and service"
They will be written following path:
/--/client\_id
/--/kms\_key\_id

(the module may add secrets over time)

Note that these values should correspond with the consuming service's tagset so secrets are placed in the path they expect. | `bool` | n/a | yes | + +## Outputs + +| Name | Description | +|------|-------------| +| [client\_id](#output\_client\_id) | n/a | +| [kms\_id](#output\_kms\_id) | n/a | + \ No newline at end of file diff --git a/kms-okta-app/fogg.tf b/kms-okta-app/fogg.tf new file mode 100644 index 00000000..8c7ad42a --- /dev/null +++ b/kms-okta-app/fogg.tf @@ -0,0 +1,2 @@ +# Auto-generated by fogg. Do not edit +# Make improvements in fogg, so that everyone can benefit. diff --git a/kms-okta-app/get_jwks_for_okta.py b/kms-okta-app/get_jwks_for_okta.py new file mode 100644 index 00000000..1ef3678e --- /dev/null +++ b/kms-okta-app/get_jwks_for_okta.py @@ -0,0 +1,28 @@ +import boto3, json +from cryptography.hazmat.primitives.serialization import load_der_public_key +from jose import jwk, constants +import sys + +aws_account_id = sys.argv[1] +kms_key_id = sys.argv[2] +region = sys.argv[3] + +sts_client = boto3.client("sts", region_name=region) +assume_role_client = sts_client.assume_role(RoleArn=f"arn:aws:iam::{aws_account_id}:role/tfe-si", RoleSessionName='FetchKMSInformation') +credentials = assume_role_client["Credentials"] +kms_session = boto3.Session( + aws_access_key_id=credentials["AccessKeyId"], + aws_secret_access_key= credentials["SecretAccessKey"], + aws_session_token= credentials["SessionToken"], + region_name=region, +) +kms_client = kms_session.client("kms", region_name=region) + +# try to get the public key in bytes +output = kms_client.get_public_key( + KeyId=kms_key_id, +) +assert output.get("PublicKey") is not None +public_key = load_der_public_key(output["PublicKey"]) +jwks_vals = jwk.RSAKey(algorithm=constants.Algorithms.RS256, key=public_key).to_dict() +print(json.dumps(jwks_vals)) diff --git a/kms-okta-app/main.tf b/kms-okta-app/main.tf new file mode 100644 index 00000000..29074ae2 --- /dev/null +++ b/kms-okta-app/main.tf @@ -0,0 +1,35 @@ +data "aws_caller_identity" "current" {} +data "aws_region" "current" {} +# uses a key in core-platform-infra to generate credentials for this workspace +data "external" "jwks_info" { + program = ["bash", "${path.module}/run.sh", path.module, data.aws_caller_identity.current.account_id, var.kms_key_id, data.aws_region.current.name] +} + +resource "okta_app_oauth" "idp_api" { + label = var.okta_configuration.label + type = var.okta_configuration.type + grant_types = var.okta_configuration.grant_types + response_types = var.okta_configuration.response_types + token_endpoint_auth_method = "private_key_jwt" + pkce_required = var.okta_configuration.pkce_required + jwks { + e = data.external.jwks_info.result.e + kty = data.external.jwks_info.result.kty + kid = var.friendly_key_identifier + n = data.external.jwks_info.result.n + } +} + + +module "params" { + source = "github.com/chanzuckerberg/cztack//aws-ssm-params-writer?ref=v0.63.3" + project = var.tags.project + env = var.tags.env + service = var.tags.service + owner = var.tags.owner + + parameters = { + "client_id" = okta_app_oauth.idp_api.client_id + "kms_key_id" = var.kms_key_id + } +} diff --git a/kms-okta-app/outputs.tf b/kms-okta-app/outputs.tf new file mode 100644 index 00000000..00c9a84e --- /dev/null +++ b/kms-okta-app/outputs.tf @@ -0,0 +1,7 @@ +output "client_id" { + value = okta_app_oauth.idp_api.id +} + +output "kms_id" { + value = var.kms_key_id +} \ No newline at end of file diff --git a/kms-okta-app/run.sh b/kms-okta-app/run.sh new file mode 100644 index 00000000..c37573a5 --- /dev/null +++ b/kms-okta-app/run.sh @@ -0,0 +1,5 @@ +#!/bin/sh + +# need to install python-jose instead of jose: https://stackoverflow.com/a/65103147 +pip install boto3 cryptography python-jose 2>&1 > /dev/null +python3 $1/get_jwks_for_okta.py $2 $3 $4 \ No newline at end of file diff --git a/kms-okta-app/terragrunt.hcl b/kms-okta-app/terragrunt.hcl new file mode 100644 index 00000000..3af9d8af --- /dev/null +++ b/kms-okta-app/terragrunt.hcl @@ -0,0 +1,4 @@ +dependencies { + paths = [ + ] +} \ No newline at end of file diff --git a/kms-okta-app/variables.tf b/kms-okta-app/variables.tf new file mode 100644 index 00000000..00d187f5 --- /dev/null +++ b/kms-okta-app/variables.tf @@ -0,0 +1,48 @@ +variable "kms_key_id" { + type = string + description = "The Key ID or alias of the AWS KMS Key. It has to be available in the same region and account as the configured provider." +} + +variable "okta_configuration" { + type = object({ + label = string + type = string + grant_types = list(string) + omit_secret = bool + response_types = list(string) + pkce_required = bool + }) + description = "Details needed to configure an okta app. Its token auth method is private_key_jwt" +} + +variable "friendly_key_identifier" { + type = string + description = "A name for the key configuration in the okta app, something you will recognize for yourself and the project." +} + +variable "write_metadata_to_params" { + type = bool + description = <--/client_id + /--/kms_key_id + + (the module may add secrets over time) + + Note that these values should correspond with the consuming service's tagset so secrets are placed in the path they expect. + EOF +} + +variable "tags" { + type = object({ + project = string, + env = string, + service = string, + owner = string, + }) + + description = <