diff --git a/.github/workflows/update-private-ca-lambda.yml b/.github/workflows/update-private-ca-lambda.yml new file mode 100644 index 0000000..8376be2 --- /dev/null +++ b/.github/workflows/update-private-ca-lambda.yml @@ -0,0 +1,34 @@ +name: Update Private CA Lambda on push + +on: + push: + branches: + - main + paths: + - private-ca/server/** + - .github/workflows/update-private-ca-lambda.yml + +permissions: + id-token: write + contents: read + +jobs: + update-lambda: + runs-on: "ubuntu-24.04" + environment: "Prod" + + steps: + - name: Checkout code + uses: actions/checkout@v3 + + - name: Configure AWS Credentials + uses: aws-actions/configure-aws-credentials@v4 + with: + aws-region: ${{ vars.AWS_REGION }} + role-to-assume: ${{ secrets.AWS_PRIVATE_CA_LAMBDA_UPDATE_ROLE }} + role-session-name: UpdatePrivateCALambda + + - name: Run update-server-on-lambda.sh + run: | + cd private-ca/ + bash update-server-on-lambda.sh diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..0979a6c --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +**/node_modules/** diff --git a/action/setup/create-role-action.sh b/action/setup/create-role-action.sh new file mode 100644 index 0000000..7d9e7cd --- /dev/null +++ b/action/setup/create-role-action.sh @@ -0,0 +1,27 @@ +ACCOUNT_ID=$1 +PROFILE=$2 +POLICY_NAME=${3:-AWS_PRIVATE_CA_LAMBDA_UPDATE_POLICY} +ROLE_NAME=${4:-AWS_PRIVATE_CA_LAMBDA_UPDATE_ROLE} + +[ ! -z $PROFILE ] && PROFILE="--profile=$PROFILE" + +ROLE_ARN=$(aws iam list-roles --query "Roles[?RoleName=='$ROLE_NAME'].Arn" --output text $PROFILE) + +if [ -n "$ROLE_ARN" ] ; then + echo "Role $ROLE_NAME already exists" +else + ASSUME_ROLE_POLICY_DOC=$( sed "s//$ACCOUNT_ID/" policies/trust-relationship-policy.json ) + ROLE_ARN=$(aws iam create-role --role-name "$ROLE_NAME" --assume-role-policy-document "$ASSUME_ROLE_POLICY_DOC" --output text $PROFILE --query 'Role.Arn') + echo "Role created with arn: " + echo $ROLE_ARN +fi + +POLICY_ARN=$(aws iam list-policies --query "Policies[?PolicyName=='$POLICY_NAME'].Arn" --output text $PROFILE) +if [ -n "$POLICY_ARN" ]; then + echo "Policy $POLICY_NAME already exists" +else + echo "Creating Policy" + POLICY_DOC=$(sed -e "s//$ACCOUNT_ID/g" policies/lambda-update-policy.json) + POLICY_ARN=$(aws iam create-policy --policy-name $POLICY_NAME --policy-document "$POLICY_DOC" $PROFILE --output text --query 'Policy.Arn' ) + aws iam attach-role-policy --role-name "$ROLE_NAME" --policy-arn $POLICY_ARN $PROFILE +fi \ No newline at end of file diff --git a/action/setup/policies/lambda-update-policy.json b/action/setup/policies/lambda-update-policy.json new file mode 100644 index 0000000..8f0c38d --- /dev/null +++ b/action/setup/policies/lambda-update-policy.json @@ -0,0 +1,15 @@ +{ + "Version": "2012-10-17", + "Statement": [ + { + "Effect": "Allow", + "Action": [ + "lambda:UpdateFunctionCode", + "lambda:GetFunction" + ], + "Resource": [ + "arn:aws:lambda:*::function:privateCA" + ] + } + ] +} \ No newline at end of file diff --git a/action/setup/policies/trust-relationship-policy.json b/action/setup/policies/trust-relationship-policy.json new file mode 100644 index 0000000..f3a7040 --- /dev/null +++ b/action/setup/policies/trust-relationship-policy.json @@ -0,0 +1,20 @@ +{ + "Version": "2012-10-17", + "Statement": [ + { + "Effect": "Allow", + "Principal": { + "Federated": "arn:aws:iam:::oidc-provider/token.actions.githubusercontent.com" + }, + "Action": "sts:AssumeRoleWithWebIdentity", + "Condition": { + "StringEquals": { + "token.actions.githubusercontent.com:aud": "sts.amazonaws.com" + }, + "StringLike": { + "token.actions.githubusercontent.com:sub": "repo:getfundwave/network-utils:environment:Prod" + } + } + } + ] +} \ No newline at end of file diff --git a/private-ca/README.md b/private-ca/README.md new file mode 100644 index 0000000..a189124 --- /dev/null +++ b/private-ca/README.md @@ -0,0 +1,160 @@ +# Private Certificate Authority (CA) for SSH Certificates + +This project provides a private Certificate Authority (CA) implementation for generating SSH certificates. It allows you to issue certificates for SSH hosts and users for secure communication. + +## Deployment + +Deploy the resources by running: + +```bash +./deploy-server-on-lambda.sh +``` + +This creates the following resources on AWS: + +- Secret to store the keys for signing certificates +- A role for the lambda function +- A policy to be attached to the role giving read access to created secret +- An openSSH layer to facilitate SSH operations +- The lambda function to act as a privateCA + +## Prerequisites for usage + +### Running via Docker (for host machines only) + +- Docker + +### Running directly + +- Python 3 +- Bash +- Dependencies: `curl`, `jq`, `ssh-keygen`, `base64` + +### Running via AWS CLI (Lambda) + +- AWS CLI +- Python 3 +- Access to the Lambda function in the specified region + +## Usage + +### Running directly + +#### For client certificates: + +```bash +bash invoke-private-ca.sh generateClientSSHCert client +``` + +#### For host certificates: + +```bash +bash invoke-private-ca.sh generateHostSSHCert host +``` + +#### For getting host CA public key: + +```bash +bash invoke-private-ca.sh getHostCAPublicKey client +``` + +**Note:** + +1. Sudo privilege is required for generating host certificates as they need to write to system directories like `/etc/ssh`. +2. The `ENVIRONMENT` (host or client) parameter affects how AWS credentials are retrieved. See [Script Parameters](#script-parameters) for more details. + +### Running via AWS CLI (Lambda) + +The `invoke-private-ca-aws-cli.sh` script provides an alternative approach to generate certificates. This method uses AWS CLI to invoke the Lambda function rather than making HTTP requests. + +#### Usage: + +```bash +bash invoke-private-ca-aws-cli.sh +``` + +### Running via Docker + +1. Build the Docker image: + + ```bash + cd client + docker build -t certificate-generator . + ``` + +2. Run the Docker container with the required volume mounts and parameters: + + ```bash + docker run --rm \ + -v $HOME/.ssh:/root/.ssh \ + -v /etc/ssh:/etc/ssh \ + certificate-generator \ + generateHostSSHCert \ + https:/// \ + host \ + ``` + +## Running as a cron job (optional) + +Since certificates need to be renewed periodically, you can set up a cron job to automatically regenerate them. + +Sample script: + +```bash +#!/bin/bash + +# Create the cron job entry +echo "* */1 * * * cd /path/to/private-ca/client && bash invoke-private-ca.sh generateHostSSHCert https:/// host >> /home/cron.log 2>&1" > /tmp/root_crontab + +# Load into root's crontab +crontab -u root /tmp/root_crontab + +# Optionally start cron service (only if not already running) +systemctl start cron 2>/dev/null || systemctl start crond 2>/dev/null +``` + +## Script Parameters + +Both `invoke-private-ca.sh` and `invoke-private-ca-aws-cli.sh` accept several shared and some script-specific parameters. + +| Parameter | Required | Description | Used In Script(s) | Default Value | +| ------------------------- | -------- | ------------------------------------------------------------------------------------------ | ------------------------------ | ------------------------- | +| `CA_ACTION` | Yes | Action to perform: `generateClientSSHCert`, `generateHostSSHCert`, or `getHostCAPublicKey` | Both | — | +| `CA_URL` | Yes | URL of the Private CA | `invoke-private-ca.sh` | — | +| `ENVIRONMENT` | No | Machine environment: `"client"` (uses AWS CLI) or `"host"` (uses EC2 metadata) | Both | `client` | +| `USER_SSH_DIR` | No | Path to user's SSH directory | Both | `$HOME/.ssh` | +| `USER_AWS_DIR` | No | Path to user's AWS directory | `invoke-private-ca.sh` | `$HOME/.aws` | +| `SYSTEM_SSH_DIR` | No | Path to system SSH directory | Both | `/etc/ssh` | +| `AWS_STS_REGION` | No | AWS region to use for STS operations | Both | `eu-central-1` | +| `LAMBDA_REGION` | No | AWS region where the Lambda function is deployed | `invoke-private-ca-aws-cli.sh` | `eu-central-1` | +| `CA_LAMBDA_FUNCTION_NAME` | No | Name of the Lambda function that performs certificate signing | `invoke-private-ca-aws-cli.sh` | `privateCA` | +| `AWS_EC2_REGION` | No | AWS region where the EC2 instance is deployed | `invoke-private-ca-aws-cli.sh` | `eu-central-1` | +| `CERT_HALF_LIFE_SECONDS` | No | Certificate half-life in seconds | Both | `259200 seconds (3 days)` | + +## Important Notes + +- **Certificate Type**: Determined by the `CA_ACTION` parameter (`generateClientSSHCert`, `generateHostSSHCert`, or `getHostCAPublicKey`) +- **Permissions**: Host certificates require sudo privileges for system directory access +- **Public Key Retrieval**: The `getHostCAPublicKey` action retrieves the Host CA's public key for host certificate verification + +## Client Environment Limitations + +**Important**: Client environments can only generate client certificates because they don't have a public IP address. + +- **Host Certificate Requirements**: Host certificates require the public IP address as a hostname when issuing the certificate. Due to the absence of a public IP address, client environments cannot generate host certificates +- **Recommendation**: Use client environments exclusively for generating client certificates, and use host environments (such as EC2 instances with public IPs) for generating host certificates + +## Directory Structure + +- `deploy-server-on-lambda.sh`: Script to deploy the Lambda function and related AWS resources +- `update-server-on-lambda.sh`: Script to update the deployed Lambda function +- `client/`: Directory containing client-side tools + - `invoke-private-ca.sh`: Main script for certificate generation using curl + - `invoke-private-ca-aws-cli.sh`: Alternative script using AWS CLI + - `aws-auth-header.py`: Python helper for generating AWS authentication headers + - `Dockerfile`: Docker container configuration +- `server/`: Directory containing server-side Lambda function code + +``` + +``` diff --git a/private-ca/client/Dockerfile b/private-ca/client/Dockerfile new file mode 100644 index 0000000..ea476de --- /dev/null +++ b/private-ca/client/Dockerfile @@ -0,0 +1,12 @@ +FROM alpine:3.18.3 + +RUN apk add bash curl jq python3 openssh + +WORKDIR /app + +COPY invoke-private-ca.sh . +COPY aws-auth-header.py . + +RUN chmod +x invoke-private-ca.sh + +ENTRYPOINT ["bash", "invoke-private-ca.sh"] \ No newline at end of file diff --git a/private-ca/client/aws-auth-header.py b/private-ca/client/aws-auth-header.py new file mode 100644 index 0000000..566e08a --- /dev/null +++ b/private-ca/client/aws-auth-header.py @@ -0,0 +1,29 @@ +import sys +from datetime import datetime, timezone +from botocore.auth import SigV4Auth +from botocore.awsrequest import AWSRequest +from botocore.credentials import Credentials + +if __name__ == "__main__": + access_key_id = sys.argv[1] + secret_access_key = sys.argv[2] + session_token = sys.argv[3] + aws_region = sys.argv[4] + + sts_host = "sts." + aws_region + ".amazonaws.com" + request_parameters = 'Action=GetCallerIdentity&Version=2011-06-15' + request_headers = { + 'Host': sts_host, + 'X-Amz-Date': datetime.now(timezone.utc).strftime('%Y%m%dT%H%M%SZ'), + 'Aud': 'FundwaveCA' + } + request = AWSRequest(method="POST", url="/", data=request_parameters, headers=request_headers) + boto_creds = Credentials(access_key_id, secret_access_key,token=session_token) + auth = SigV4Auth(boto_creds, "sts", aws_region) + auth.add_auth(request) + + authorization = request.headers["Authorization"] + date = request.headers["X-Amz-Date"] + + response = f'{{"Authorization": "{authorization}", "Date": "{date}"}}' + print(response) \ No newline at end of file diff --git a/private-ca/client/invoke-private-ca-aws-cli.sh b/private-ca/client/invoke-private-ca-aws-cli.sh new file mode 100755 index 0000000..c4d704f --- /dev/null +++ b/private-ca/client/invoke-private-ca-aws-cli.sh @@ -0,0 +1,341 @@ +#!/bin/bash + +CA_ACTION=${1} +ENVIRONMENT=${2:-"client"} +AWS_EC2_REGION=${3:-"eu-central-1"} +USER_SSH_DIR=${4:-"$HOME/.ssh"} +USER_AWS_DIR=${5:-"$HOME/.aws"} +SYSTEM_SSH_DIR=${6:-"/etc/ssh"} +CA_LAMBDA_FUNCTION_NAME=${7:-"privateCA"} +LAMBDA_REGION=${8:-'eu-central-1'} +AWS_STS_REGION=${9:-"eu-central-1"} + +CERT_HALF_LIFE_SECONDS=${10:-$((3 * 24 * 60 * 60))} + +PYTHON_EXEC=$(which python 2>/dev/null || which python3 2>/dev/null) +[[ $? -ne 0 ]] && { echo "Python binary not found."; exit 1; } + +trap 'clean_config_on_error' EXIT + +clean_config_on_error() { + rm -f private-ca-client-response.json private-ca-client-event.json + if [[ $? -ne 0 && $CA_ACTION == "generateHostSSHCert" ]]; then + if [[ "$CERT_VALID" == "true" ]]; then + echo "Keeping existing valid certificate." + else + echo "Certificate generation failed and current cert would expire before next run. Cleaning host SSH config..." + sed -i "\|^HostCertificate ${SYSTEM_SSH_DIR}/ssh_host_rsa_key-cert.pub\$|d" ${SYSTEM_SSH_DIR}/sshd_config + sed -i "\|^TrustedUserCAKeys ${SYSTEM_SSH_DIR}/user_ca.pub\$|d" ${SYSTEM_SSH_DIR}/sshd_config + + systemctl restart sshd + echo "Host SSH config cleaned." + fi + fi +} + +get_aws_credentials() { + local TEMP_CREDS + + if [[ $ENVIRONMENT == "host" ]]; then + TOKEN=$(curl -s --max-time 30 -X PUT "http://169.254.169.254/latest/api/token" -H "X-aws-ec2-metadata-token-ttl-seconds: 120") + + if [[ -z "$TOKEN" ]]; then + echo "Failed to fetch EC2 metadata token. Are you running this script on an EC2 instance?" + exit 1 + fi + + INSTANCE_ROLE_NAME=$(curl -H "X-aws-ec2-metadata-token: $TOKEN" http://169.254.169.254/latest/meta-data/iam/security-credentials/) + TEMP_CREDS=$(curl -H "X-aws-ec2-metadata-token: $TOKEN" http://169.254.169.254/latest/meta-data/iam/security-credentials/$INSTANCE_ROLE_NAME) + + elif [[ $ENVIRONMENT == "client" ]]; then + if [[ -n "$AWS_ACCESS_KEY_ID" && -n "$AWS_SECRET_ACCESS_KEY" && -n "$AWS_SESSION_TOKEN" ]]; then + CALLER_IDENTITY=$(aws sts get-caller-identity) + [[ $? -ne 0 ]] && { echo "Your AWS credentials have either expired or are invalid. Please check your credentials and try again."; exit 1; } + TEMP_CREDS=$(echo "{\"AccessKeyId\":\"$AWS_ACCESS_KEY_ID\",\"SecretAccessKey\":\"$AWS_SECRET_ACCESS_KEY\",\"Token\":\"$AWS_SESSION_TOKEN\"}") + else + TEMP_CREDS=$(aws configure export-credentials 2>/dev/null) + SESSION_TOKEN=$(echo "$TEMP_CREDS" | jq -r '.Token // .SessionToken // .Sessiontoken // empty') + [[ -z "$SESSION_TOKEN" ]] && TEMP_CREDS=$(aws sts get-session-token | jq -r ".Credentials") + fi + else + echo "Invalid environment provided. Allowed values are 'host' and 'client'"; exit 1; + fi + + ACCESS_KEY_ID=$(echo $TEMP_CREDS | jq -r ".AccessKeyId") + SECRET_ACCESS_KEY=$(echo $TEMP_CREDS | jq -r ".SecretAccessKey") + SESSION_TOKEN=$(echo $TEMP_CREDS | jq -r ".Token // .SessionToken // .Sessiontoken") +} + +prepare_event_json() { + # Setup Python virtual environment + if [ ! -d "private-ca-client-env" ]; then + $PYTHON_EXEC -m venv private-ca-client-env + fi + source ./private-ca-client-env/bin/activate + pip install -q --upgrade --disable-pip-version-check boto3 + + # Update PYTHON_EXEC to use the Python executable from the activated virtual environment + # This ensures we use the venv's Python with the installed dependencies (boto3) + PYTHON_EXEC=$(which python 2>/dev/null || which python3 2>/dev/null) + + # Generate Auth Headers + output=$($PYTHON_EXEC aws-auth-header.py $ACCESS_KEY_ID $SECRET_ACCESS_KEY $SESSION_TOKEN $AWS_STS_REGION) || { + echo "Failed to generate auth header. Check aws-auth-header.py script."; + exit 1; + } + auth_header=$(echo "$output" | jq -er ".Authorization") || { + echo "Failed to parse Authorization from auth-header output."; + exit 1; + } + date=$(echo "$output" | jq -er ".Date") || { + echo "Failed to parse Date from auth-header output."; + exit 1; + } + + INNER_JSON=$(jq -n \ + --arg amzDate "$date" \ + --arg authHeader "$auth_header" \ + --arg sessionToken "$SESSION_TOKEN" \ + --arg certPubkey "$CERT_PUBKEY" \ + --arg action "$CA_ACTION" \ + --arg awsRegion "$AWS_STS_REGION" \ + --arg awsEC2Region "$AWS_EC2_REGION" \ + '{ + auth: { + amzDate: $amzDate, + authorizationHeader: $authHeader, + sessionToken: $sessionToken + }, + certPubkey: $certPubkey, + action: $action, + awsSTSRegion: $awsRegion, + awsEC2Region: $awsEC2Region + }' | jq -c) + + # JSON with body as stringified JSON as required by server + json_body=$(jq -n --arg body "$INNER_JSON" '{body: $body}') + echo "$json_body" > private-ca-client-event.json +} + +invoke_lambda() { + INVOKE_OUTPUT=$(aws lambda invoke \ + --function-name ${CA_LAMBDA_FUNCTION_NAME} \ + --cli-binary-format raw-in-base64-out \ + --payload file://private-ca-client-event.json \ + private-ca-client-response.json \ + --region $LAMBDA_REGION) || { + echo "$INVOKE_OUTPUT" + echo "Lambda invocation failed" + exit 1 + } + + response_body=$(cat private-ca-client-response.json | jq -r ".body") || { + echo "Failed to parse response body."; + exit 1 + } + status_code=$(cat private-ca-client-response.json | jq -r ".statusCode") || { + echo "Failed to parse status code."; + exit 1 + } + + if [[ $status_code -ne 200 ]]; then + echo "CA request failed (Status: ${status_code}): ${response_body}" + exit 1 + fi + + # If the action is getHostCAPublicKey, we don't get a certificate + [[ $CA_ACTION == "getHostCAPublicKey" ]] && return + + ENCODED_CERTIFICATE=$(echo "$response_body" | jq -er ".certificate") || { + echo "Certificate not found in Lambda response. Aborting."; + exit 1; + } + + CERTIFICATE=$(echo $ENCODED_CERTIFICATE | base64 -d) + + if [[ -z "$CERTIFICATE" ]]; then + echo "Invalid certificate received. Aborting." + exit 1 + fi +} + +safe_replace_old_certificate() { + local CERTIFICATE=${1} + local CERT_FILE_PATH=${2} + + local TEMP_CERT_FILE="${CERT_FILE_PATH}.tmp" + + echo "$CERTIFICATE" > "$TEMP_CERT_FILE" + + # Verify the new certificate is valid + if ssh-keygen -Lf "$TEMP_CERT_FILE" >/dev/null 2>&1; then + mv "$TEMP_CERT_FILE" "${CERT_FILE_PATH}" + echo "New certificate written to ${CERT_FILE_PATH}" + CERT_VALID=true + else + rm -f "$TEMP_CERT_FILE" + echo "Generated certificate is invalid. Discarding." + exit 1 + fi +} + +# Check for options +while getopts ":h" option; do + case $option in + h) + echo "Usage: bash invoke-private-ca-aws-cli.sh [ACTION] [ENVIRONMENT] [AWS_EC2_REGION] [AWS PROFILE] [USER SSH DIR] [USER AWS DIR] [SYSTEM SSH DIR] [CA_LAMBDA_FUNCTION_NAME] [LAMBDA_REGION] [AWS STS REGION] [CERT HALF LIFE SECONDS]" + echo "" + echo "Actions:" + echo " generateHostSSHCert Generates SSH Certificate for Host" + echo " generateClientSSHCert Generates SSH Certificate for Client" + echo " getHostCAPublicKey Gets Host CA Public Key" + echo "" + echo "Parameters:" + echo " ENVIRONMENT Environment to use (default: client)" + echo " AWS PROFILE AWS profile to use (default: default)" + echo " USER SSH DIR Path to user's SSH directory (default: /home/$USER/.ssh)" + echo " USER AWS DIR Path to user's AWS directory (default: /home/$USER/.aws)" + echo " SYSTEM SSH DIR Path to system's SSH directory (default: /etc/ssh)" + echo " AWS STS REGION AWS region for STS operations (default: ap-southeast-1)" + exit;; + *) + echo "Error: Invalid option" + exit;; + esac +done + + +if [[ $CA_ACTION = "generateClientSSHCert" ]]; then + [[ -d "${USER_SSH_DIR}" ]] || { echo "User SSH directory does not exist. Please provide the correct user SSH directory."; exit 1; } + + # Check if a valid certificate exists + CERT_VALID=false + if test -f ${USER_SSH_DIR}/id_rsa-cert.pub; then + # Client SSH Certificate already exists + current_timestamp=$(date -u +%s) + certificate_expiration_timestamp=$(TZ=UTC ssh-keygen -Lf ${USER_SSH_DIR}/id_rsa-cert.pub 2>/dev/null | awk '/Valid:/{print $NF}') + + if [[ $certificate_expiration_timestamp > $current_timestamp ]]; then + # Certificate is valid + if [[ -f "${USER_SSH_DIR}/known_hosts" ]]; then + if grep -qE '^@cert-authority .* fundwave_host_ca$' "${USER_SSH_DIR}/known_hosts"; then + CERT_VALID=true + echo "A valid user certificate and known_hosts entry were found." + else + echo "User certificate is valid, but known_hosts entry is missing." + fi + else + echo "User certificate is valid, but known_hosts file is missing." + fi + else + echo "Existing user certificate is expired or invalid." + rm -f ${USER_SSH_DIR}/id_rsa-cert.pub + fi + fi + test -f ${USER_SSH_DIR}/id_rsa.pub || { + ssh-keygen -t rsa -b 4096 -f ${USER_SSH_DIR}/id_rsa -N "" + [[ -f ${USER_SSH_DIR}/id_rsa-cert.pub ]] && rm ${USER_SSH_DIR}/id_rsa-cert.pub + } + CERT_PUBKEY=$(cat ${USER_SSH_DIR}/id_rsa.pub | base64 | tr -d \\n) + + get_aws_credentials + prepare_event_json + invoke_lambda + + HOST_CA_PUBKEY=$(echo $response_body | jq -r ".\"host_ca.pub\"" | base64 -d) + + safe_replace_old_certificate "$CERTIFICATE" "${USER_SSH_DIR}/id_rsa-cert.pub" + + [[ -f "${USER_SSH_DIR}/known_hosts" ]] || touch "${USER_SSH_DIR}/known_hosts" + + # Add host CA public key to known_hosts file if it doesn't exist and update it if it does + # @cert-authority tells ssh to trust the host CA public key + # ${HOST_CA_PUBKEY} is the host CA public key that was used to sign the host certificate + if grep -qE '^@cert-authority .* fundwave_host_ca$' "${USER_SSH_DIR}/known_hosts"; then + # Update existing line + sed -i.bak -E "s|^(@cert-authority .*) ssh-rsa .*|\1 ${HOST_CA_PUBKEY}|" "${USER_SSH_DIR}/known_hosts" + else + # * means all hosts (wildcard) (you can also specify a list of comma separated hostnames) + echo "@cert-authority * ${HOST_CA_PUBKEY}" >> "${USER_SSH_DIR}/known_hosts" + fi + +elif [[ $CA_ACTION = "generateHostSSHCert" ]]; then + # Host certificate generation is not allowed in client environment + if [[ $ENVIRONMENT = "client" ]]; then + echo -e "\nError: generateHostSSHCert is not allowed in client environment.\nHost certificate generation requires host (server) environment.\n" + exit 1 + fi + if [ "$EUID" -ne 0 ]; then + echo "Run this script with sudo or as root for generating host certificate." + exit 1 + fi + + # Check if a valid certificate exists + CERT_VALID=false + if test -f ${SYSTEM_SSH_DIR}/ssh_host_rsa_key-cert.pub; then + current_timestamp=$(date -u +%s) + certificate_expiration_timestamp=$(TZ=UTC ssh-keygen -Lf ${SYSTEM_SSH_DIR}/ssh_host_rsa_key-cert.pub 2>/dev/null | awk '/Valid:/{print $NF}') + [[ $(uname) == "Darwin" ]] && cert_expiry_epoch=$(date -j -f "%Y-%m-%dT%H:%M:%S" "$certificate_expiration_timestamp" +"%s") || cert_expiry_epoch=$(date -d "$certificate_expiration_timestamp" +"%s") + next_run_timestamp=$((current_timestamp + CERT_HALF_LIFE_SECONDS)) + + if [[ $cert_expiry_epoch -gt $next_run_timestamp ]]; then + CERT_VALID=true + echo "A valid host certificate was found at ${SYSTEM_SSH_DIR}/ssh_host_rsa_key-cert.pub." + else + echo "Existing host certificate will expire before next cron run." + rm -f ${SYSTEM_SSH_DIR}/ssh_host_rsa_key-cert.pub + fi + fi + + test -f ${SYSTEM_SSH_DIR}/ssh_host_rsa_key.pub || { + ssh-keygen -t rsa -b 4096 -f ${SYSTEM_SSH_DIR}/ssh_host_rsa_key -N "" + [[ -f ${SYSTEM_SSH_DIR}/ssh_host_rsa_key-cert.pub ]] && rm ${SYSTEM_SSH_DIR}/ssh_host_rsa_key-cert.pub + } + CERT_PUBKEY=$(cat ${SYSTEM_SSH_DIR}/ssh_host_rsa_key.pub | base64 | tr -d \\n) + + get_aws_credentials + prepare_event_json + invoke_lambda + + USER_CA_PUBKEY=$(echo $response_body | jq -r ".\"user_ca.pub\"" | base64 -d) + + safe_replace_old_certificate "$CERTIFICATE" "${SYSTEM_SSH_DIR}/ssh_host_rsa_key-cert.pub" + + [[ -f "${SYSTEM_SSH_DIR}/user_ca.pub" ]] || touch "${SYSTEM_SSH_DIR}/user_ca.pub" + + if grep -qE '.* fundwave_user_ca$' "${SYSTEM_SSH_DIR}/user_ca.pub"; then + # Update existing line + sed -i.bak -E "s|ssh-rsa .* fundwave_user_ca$|${USER_CA_PUBKEY}|" "${SYSTEM_SSH_DIR}/user_ca.pub" + else + echo "${USER_CA_PUBKEY}" >> "${SYSTEM_SSH_DIR}/user_ca.pub" + fi + + if [[ $(grep -q "HostCertificate" "${SYSTEM_SSH_DIR}/sshd_config"; echo $?) -ne 0 ]]; then + echo "HostCertificate ${SYSTEM_SSH_DIR}/ssh_host_rsa_key-cert.pub" >> ${SYSTEM_SSH_DIR}/sshd_config + fi + + if [[ $(grep -q "TrustedUserCAKeys" "${SYSTEM_SSH_DIR}/sshd_config"; echo $?) -ne 0 ]]; then + echo "TrustedUserCAKeys ${SYSTEM_SSH_DIR}/user_ca.pub" >> ${SYSTEM_SSH_DIR}/sshd_config + fi + systemctl restart sshd + +elif [[ $CA_ACTION = "getHostCAPublicKey" ]]; then + get_aws_credentials + prepare_event_json + invoke_lambda + + HOST_CA_PUBKEY=$(echo $response_body | jq -r ".\"host_ca.pub\"" | base64 -d) + + [[ -f "${USER_SSH_DIR}/known_hosts" ]] || touch "${USER_SSH_DIR}/known_hosts" + + if grep -qE '^@cert-authority .* fundwave_host_ca$' "${USER_SSH_DIR}/known_hosts"; then + sed -i.bak -E "s|^(@cert-authority .*) ssh-rsa .*|\1 ${HOST_CA_PUBKEY}|" "${USER_SSH_DIR}/known_hosts" + else + echo "@cert-authority * ${HOST_CA_PUBKEY}" >> "${USER_SSH_DIR}/known_hosts" + fi + echo "Host CA public key added to known_hosts." + +fi + +# Clean up +deactivate diff --git a/private-ca/client/invoke-private-ca.sh b/private-ca/client/invoke-private-ca.sh new file mode 100644 index 0000000..5849a4f --- /dev/null +++ b/private-ca/client/invoke-private-ca.sh @@ -0,0 +1,305 @@ +#!/bin/bash + +CA_ACTION=${1} +CA_URL=${2} +ENVIRONMENT=${3:-"client"} +AWS_EC2_REGION=${4:-"eu-central-1"} +USER_SSH_DIR=${5:-"$HOME/.ssh"} +USER_AWS_DIR=${6:-"$HOME/.aws"} +SYSTEM_SSH_DIR=${7:-"/etc/ssh"} +AWS_STS_REGION=${8:-"eu-central-1"} +CERT_HALF_LIFE_SECONDS=${9:-$((3 * 24 * 60 * 60))} + +PYTHON_EXEC=$(which python 2>/dev/null || which python3 2>/dev/null) +[[ $? -ne 0 ]] && { echo "Python binary not found."; exit 1; } + +trap 'clean_config_on_error' EXIT + +clean_config_on_error() { + if [[ $? -ne 0 && $CA_ACTION == "generateHostSSHCert" ]]; then + if [[ "$CERT_VALID" == "true" ]]; then + echo "Keeping existing valid certificate." + else + echo "Certificate generation failed and current cert would expire before next run. Cleaning host SSH config..." + sed -i "\|^HostCertificate ${SYSTEM_SSH_DIR}/ssh_host_rsa_key-cert.pub\$|d" ${SYSTEM_SSH_DIR}/sshd_config + sed -i "\|^TrustedUserCAKeys ${SYSTEM_SSH_DIR}/user_ca.pub\$|d" ${SYSTEM_SSH_DIR}/sshd_config + + systemctl restart sshd + echo "Host SSH config cleaned." + fi + fi +} + +get_aws_credentials() { + local TEMP_CREDS + + if [[ $ENVIRONMENT == "host" ]]; then + TOKEN=$(curl -s --max-time 30 -X PUT "http://169.254.169.254/latest/api/token" -H "X-aws-ec2-metadata-token-ttl-seconds: 120") + + if [[ -z "$TOKEN" ]]; then + echo "Failed to fetch EC2 metadata token. Are you running this script on an EC2 instance?" + exit 1 + fi + + INSTANCE_ROLE_NAME=$(curl -H "X-aws-ec2-metadata-token: $TOKEN" http://169.254.169.254/latest/meta-data/iam/security-credentials/) + TEMP_CREDS=$(curl -H "X-aws-ec2-metadata-token: $TOKEN" http://169.254.169.254/latest/meta-data/iam/security-credentials/$INSTANCE_ROLE_NAME) + + elif [[ $ENVIRONMENT == "client" ]]; then + if [[ -n "$AWS_ACCESS_KEY_ID" && -n "$AWS_SECRET_ACCESS_KEY" && -n "$AWS_SESSION_TOKEN" ]]; then + CALLER_IDENTITY=$(aws sts get-caller-identity) + [[ $? -ne 0 ]] && { echo "Your AWS credentials have either expired or are invalid. Please check your credentials and try again."; exit 1; } + TEMP_CREDS=$(echo "{\"AccessKeyId\":\"$AWS_ACCESS_KEY_ID\",\"SecretAccessKey\":\"$AWS_SECRET_ACCESS_KEY\",\"Token\":\"$AWS_SESSION_TOKEN\"}") + else + TEMP_CREDS=$(aws configure export-credentials 2>/dev/null) + SESSION_TOKEN=$(echo "$TEMP_CREDS" | jq -r '.Token // .SessionToken // .Sessiontoken // empty') + [[ -z "$SESSION_TOKEN" ]] && TEMP_CREDS=$(aws sts get-session-token | jq -r ".Credentials") + fi + else + echo "Invalid environment provided. Allowed values are 'host' and 'client'"; exit 1; + fi + + ACCESS_KEY_ID=$(echo $TEMP_CREDS | jq -r ".AccessKeyId") + SECRET_ACCESS_KEY=$(echo $TEMP_CREDS | jq -r ".SecretAccessKey") + SESSION_TOKEN=$(echo $TEMP_CREDS | jq -r ".Token // .SessionToken // .Sessiontoken") +} + +prepare_event_json() { + # Setup Python virtual environment + if [ ! -d "private-ca-client-env" ]; then + $PYTHON_EXEC -m venv private-ca-client-env + fi + source ./private-ca-client-env/bin/activate + pip install -q --upgrade --disable-pip-version-check boto3 + + # Update PYTHON_EXEC to use the Python executable from the activated virtual environment + # This ensures we use the venv's Python with the installed dependencies (boto3) + PYTHON_EXEC=$(which python 2>/dev/null || which python3 2>/dev/null) + + # Generate Auth Headers + output=$($PYTHON_EXEC aws-auth-header.py $ACCESS_KEY_ID $SECRET_ACCESS_KEY $SESSION_TOKEN $AWS_STS_REGION) || { + echo "Failed to generate auth header. Check aws-auth-header.py script."; + exit 1; + } + auth_header=$(echo "$output" | jq -er ".Authorization") || { + echo "Failed to parse Authorization from auth-header output."; + exit 1; + } + date=$(echo "$output" | jq -er ".Date") || { + echo "Failed to parse Date from auth-header output."; + exit 1; + } + + # Create event JSON for CA request + EVENT_JSON=$(echo "{\"auth\":{\"amzDate\":\"${date}\",\"authorizationHeader\":\"${auth_header}\",\"sessionToken\":\"${SESSION_TOKEN}\"},\"certPubkey\":\"${CERT_PUBKEY}\",\"action\":\"${CA_ACTION}\",\"awsSTSRegion\":\"${AWS_STS_REGION}\",\"awsEC2Region\":\"${AWS_EC2_REGION}\"}") +} + +invoke_lambda() { + read -r STATUS_CODE LAMBDA_RESPONSE < <( + curl -s "${CA_URL}" -H 'content-type: application/json' -d "$EVENT_JSON" -w "%{http_code}\n" | + { + response=$(cat) + status_code=${response: -3} + body=${response:0:$((${#response}-3))} + echo "$status_code $body" + } + ) + + if [[ "$STATUS_CODE" != "200" ]]; then + echo "CA request failed (Status: ${STATUS_CODE}): ${LAMBDA_RESPONSE}" + exit 1; + fi + + # If the action is getHostCAPublicKey, we don't get a certificate + [[ $CA_ACTION == "getHostCAPublicKey" ]] && return + + ENCODED_CERTIFICATE=$(echo "$LAMBDA_RESPONSE" | jq -er ".certificate") || { + echo "Certificate not found in Lambda response. Aborting."; + exit 1; + } + + CERTIFICATE=$(echo $ENCODED_CERTIFICATE | base64 -d) + + if [[ -z "$CERTIFICATE" ]]; then + echo "Invalid certificate received. Aborting." + exit 1 + fi +} + +safe_replace_old_certificate() { + local CERTIFICATE=${1} + local CERT_FILE_PATH=${2} + + local TEMP_CERT_FILE="${CERT_FILE_PATH}.tmp" + + echo "$CERTIFICATE" > "$TEMP_CERT_FILE" + + # Verify the new certificate is valid + if ssh-keygen -Lf "$TEMP_CERT_FILE" >/dev/null 2>&1; then + mv "$TEMP_CERT_FILE" "${CERT_FILE_PATH}" + echo "New certificate written to ${CERT_FILE_PATH}" + CERT_VALID=true + else + rm -f "$TEMP_CERT_FILE" + echo "Generated certificate is invalid. Discarding." + exit 1 + fi +} + +# Check for options +while getopts ":h" option; do + case $option in + h) + echo "Usage: bash invoke-private-ca.sh [ACTION] [CA URL] [ENVIRONMENT] [AWS EC2 REGION] [USER SSH DIR] [USER AWS DIR] [SYSTEM SSH DIR] [AWS STS REGION] [CERT HALF LIFE SECONDS]" + echo "Possible actions:" + echo " generateHostSSHCert: Generates SSH Certificate for Host" + echo " generateClientSSHCert: Generates SSH Certificate for Client" + echo " getHostCAPublicKey: Gets the Host CA Public Key" + exit;; + *) + echo "Error: Invalid option" + exit;; + esac +done + +# Check for CA Action +if [[ $CA_ACTION = "generateClientSSHCert" ]]; then + + [[ -d "${USER_SSH_DIR}" ]] || { echo "User SSH directory does not exist. Please provide the correct user SSH directory."; exit 1; } + + # Check if a valid certificate exists + CERT_VALID=false + if test -f ${USER_SSH_DIR}/id_rsa-cert.pub; then + # Client SSH Certificate already exists + current_timestamp=$(date -u +%s) + certificate_expiration_timestamp=$(TZ=UTC ssh-keygen -Lf ${USER_SSH_DIR}/id_rsa-cert.pub 2>/dev/null | awk '/Valid:/{print $NF}') + + if [[ $certificate_expiration_timestamp > $current_timestamp ]]; then + # Certificate is valid + if [[ -f "${USER_SSH_DIR}/known_hosts" ]]; then + if grep -qE '^@cert-authority .* fundwave_host_ca$' "${USER_SSH_DIR}/known_hosts"; then + CERT_VALID=true + echo "A valid user certificate and known_hosts entry were found." + else + echo "User certificate is valid, but known_hosts entry is missing." + fi + else + echo "User certificate is valid, but known_hosts file is missing." + fi + else + echo "Existing user certificate is expired or invalid." + rm -f ${USER_SSH_DIR}/id_rsa-cert.pub + fi + fi + test -f ${USER_SSH_DIR}/id_rsa.pub || { + ssh-keygen -t rsa -b 4096 -f ${USER_SSH_DIR}/id_rsa -N "" + [[ -f ${USER_SSH_DIR}/id_rsa-cert.pub ]] && rm ${USER_SSH_DIR}/id_rsa-cert.pub + } + CERT_PUBKEY=$(cat ${USER_SSH_DIR}/id_rsa.pub | base64 | tr -d \\n) + + get_aws_credentials + prepare_event_json + invoke_lambda + + HOST_CA_PUBKEY=$(echo $LAMBDA_RESPONSE | jq -r ".\"host_ca.pub\"" | base64 -d) + + safe_replace_old_certificate "$CERTIFICATE" "${USER_SSH_DIR}/id_rsa-cert.pub" + + [[ -f "${USER_SSH_DIR}/known_hosts" ]] || touch "${USER_SSH_DIR}/known_hosts" + + # Add host CA public key to known_hosts file if it doesn't exist and update it if it does + # @cert-authority tells ssh to trust the host CA public key + # ${HOST_CA_PUBKEY} is the host CA public key that was used to sign the host certificate + if grep -qE '^@cert-authority .* fundwave_host_ca$' "${USER_SSH_DIR}/known_hosts"; then + # Update existing line + sed -i.bak -E "s|^(@cert-authority .*) ssh-rsa .*|\1 ${HOST_CA_PUBKEY}|" "${USER_SSH_DIR}/known_hosts" + else + # * means all hosts (wildcard) (you can also specify a list of comma separated hostnames) + echo "@cert-authority * ${HOST_CA_PUBKEY}" >> "${USER_SSH_DIR}/known_hosts" + fi + +elif [[ $CA_ACTION = "generateHostSSHCert" ]]; then + # Host certificate generation is not allowed in client environment + if [[ $ENVIRONMENT = "client" ]]; then + echo -e "\nError: generateHostSSHCert is not allowed in client environment.\nHost certificate generation requires host (server) environment.\n" + exit 1 + fi + if [ "$EUID" -ne 0 ]; then + echo "Run this script with sudo or as root for generating host certificate." + exit 1 + fi + + # Check if a valid certificate exists + CERT_VALID=false + if test -f ${SYSTEM_SSH_DIR}/ssh_host_rsa_key-cert.pub; then + current_timestamp=$(date -u +%s) + certificate_expiration_timestamp=$(TZ=UTC ssh-keygen -Lf ${SYSTEM_SSH_DIR}/ssh_host_rsa_key-cert.pub 2>/dev/null | awk '/Valid:/{print $NF}') + [[ $(uname) == "Darwin" ]] && cert_expiry_epoch=$(date -j -f "%Y-%m-%dT%H:%M:%S" "$certificate_expiration_timestamp" +"%s") || cert_expiry_epoch=$(date -d "$certificate_expiration_timestamp" +"%s") + next_run_timestamp=$((current_timestamp + CERT_HALF_LIFE_SECONDS)) + + if [[ $cert_expiry_epoch -gt $next_run_timestamp ]]; then + CERT_VALID=true + echo "A valid host certificate was found at ${SYSTEM_SSH_DIR}/ssh_host_rsa_key-cert.pub." + else + echo "Existing host certificate will expire before next cron run." + rm -f ${SYSTEM_SSH_DIR}/ssh_host_rsa_key-cert.pub + fi + fi + + test -f ${SYSTEM_SSH_DIR}/ssh_host_rsa_key.pub || { + ssh-keygen -t rsa -b 4096 -f ${SYSTEM_SSH_DIR}/ssh_host_rsa_key -N "" + [[ -f ${SYSTEM_SSH_DIR}/ssh_host_rsa_key-cert.pub ]] && rm ${SYSTEM_SSH_DIR}/ssh_host_rsa_key-cert.pub + } + CERT_PUBKEY=$(cat ${SYSTEM_SSH_DIR}/ssh_host_rsa_key.pub | base64 | tr -d \\n) + + get_aws_credentials + prepare_event_json + invoke_lambda + + USER_CA_PUBKEY=$(echo $LAMBDA_RESPONSE | jq -r ".\"user_ca.pub\"" | base64 -d) + + safe_replace_old_certificate "$CERTIFICATE" "${SYSTEM_SSH_DIR}/ssh_host_rsa_key-cert.pub" + + [[ -f "${SYSTEM_SSH_DIR}/user_ca.pub" ]] || touch "${SYSTEM_SSH_DIR}/user_ca.pub" + + if grep -qE '.* fundwave_user_ca$' "${SYSTEM_SSH_DIR}/user_ca.pub"; then + # Update existing line + sed -i.bak -E "s|ssh-rsa .* fundwave_user_ca$|${USER_CA_PUBKEY}|" "${SYSTEM_SSH_DIR}/user_ca.pub" + else + echo "${USER_CA_PUBKEY}" >> "${SYSTEM_SSH_DIR}/user_ca.pub" + fi + + if [[ $(grep -q "HostCertificate" "${SYSTEM_SSH_DIR}/sshd_config"; echo $?) -ne 0 ]]; then + echo "HostCertificate ${SYSTEM_SSH_DIR}/ssh_host_rsa_key-cert.pub" >> ${SYSTEM_SSH_DIR}/sshd_config + fi + + if [[ $(grep -q "TrustedUserCAKeys" "${SYSTEM_SSH_DIR}/sshd_config"; echo $?) -ne 0 ]]; then + echo "TrustedUserCAKeys ${SYSTEM_SSH_DIR}/user_ca.pub" >> ${SYSTEM_SSH_DIR}/sshd_config + fi + systemctl restart sshd + +elif [[ $CA_ACTION = "getHostCAPublicKey" ]]; then + get_aws_credentials + prepare_event_json + invoke_lambda + + HOST_CA_PUBKEY=$(echo $LAMBDA_RESPONSE | jq -r ".\"host_ca.pub\"" | base64 -d) + + [[ -f "${USER_SSH_DIR}/known_hosts" ]] || touch "${USER_SSH_DIR}/known_hosts" + + if grep -qE '^@cert-authority .* fundwave_host_ca$' "${USER_SSH_DIR}/known_hosts"; then + sed -i.bak -E "s|^(@cert-authority .*) ssh-rsa .*|\1 ${HOST_CA_PUBKEY}|" "${USER_SSH_DIR}/known_hosts" + else + echo "@cert-authority * ${HOST_CA_PUBKEY}" >> "${USER_SSH_DIR}/known_hosts" + fi + echo "Host CA public key added to known_hosts." + +else + echo "Invalid Action" + echo "Possible actions include:" + echo " generateHostSSHCert: Generates SSH Certificate for Host" + echo " generateClientSSHCert: Generates SSH Certificate for Client" + echo " getHostCAPublicKey: Gets the Host CA Public Key" + exit 1; +fi + +deactivate \ No newline at end of file diff --git a/private-ca/deploy-server-on-lambda.sh b/private-ca/deploy-server-on-lambda.sh new file mode 100755 index 0000000..78ba953 --- /dev/null +++ b/private-ca/deploy-server-on-lambda.sh @@ -0,0 +1,118 @@ +#!/bin/bash +trap 'rm host_ca host_ca.pub user_ca user_ca.pub secret.json' EXIT + +SECRET_PREFIX=${1:-"privateCA"} +ROLE_NAME=${2:-"privateCALambdaRole"} +POLICY_NAME=${3:-"PrivateCAPolicy"} +LAYER_NAME=${4:-"openssh"} +FUNCTION_NAME=${5:-"privateCA"} +AWS_REGION=${6:-"eu-central-1"} +AWS_PROFILE=${7:-"default"} +################## Secret ################## + +ACCOUNT_ID=$(aws sts get-caller-identity --profile $AWS_PROFILE | jq -r ".Account") + +# Generate Keys +ssh-keygen -t rsa -b 4096 -f host_ca -C fundwave_host_ca -N "" +ssh-keygen -t rsa -b 4096 -f user_ca -C fundwave_user_ca -N "" + +HOST_CA_PRIVATE_KEY=$(cat host_ca | base64 | tr -d \\n) +HOST_CA_PUBLIC_KEY=$(cat host_ca.pub | base64 | tr -d \\n) +USER_CA_PRIVATE_KEY=$(cat user_ca | base64 | tr -d \\n) +USER_CA_PUBLIC_KEY=$(cat user_ca.pub | base64 | tr -d \\n) + +echo "{\"host_ca\": \"${HOST_CA_PRIVATE_KEY}\", \"host_ca.pub\": \"${HOST_CA_PUBLIC_KEY}\", \"user_ca\": \"${USER_CA_PRIVATE_KEY}\",\"user_ca.pub\": \"${USER_CA_PUBLIC_KEY}\"}" | jq . > secret.json + +SECRET_NAME="${SECRET_PREFIX}_${ACCOUNT_ID}_secret" + +# Create Secret +SECRET_ARN=$(aws secretsmanager create-secret \ + --name $SECRET_NAME \ + --secret-string file://secret.json \ + --region $AWS_REGION \ + --profile $AWS_PROFILE \ + | jq ".ARN" | tr -d '"') + +# Clean up +rm host_ca host_ca.pub user_ca user_ca.pub secret.json +############################################ + +################### Role ################### + +# Create role for lambda +echo "{\"Version\": \"2012-10-17\",\"Statement\": [{\"Sid\": \"AllowLambdaAssumeRole\",\"Effect\": \"Allow\",\"Principal\": {\"Service\": \"lambda.amazonaws.com\"},\"Action\": \"sts:AssumeRole\"}]}" | jq . > Trust-Policy.json + +ROLE_ARN=$(aws iam create-role \ + --role-name $ROLE_NAME \ + --region $AWS_REGION \ + --profile $AWS_PROFILE \ + --assume-role-policy-document file://Trust-Policy.json | jq ".Role.Arn" | tr -d '"') + +# Create Policy for Lambda Role to Read and Update Secrets +echo "{\"Version\": \"2012-10-17\",\"Statement\": [{\"Sid\": \"VisualEditor0\",\"Effect\": \"Allow\",\"Action\": [\"secretsmanager:GetSecretValue\"],\"Resource\": \"${SECRET_ARN}\"}, {\"Action\": [\"logs:CreateLogGroup\",\"logs:CreateLogStream\",\"logs:PutLogEvents\"],\"Effect\": \"Allow\",\"Resource\": \"arn:aws:logs:*:*:*\"}]}" | jq . > Policy.json + +POLICY_ARN=$(aws iam create-policy --policy-name $POLICY_NAME --region $AWS_REGION --profile $AWS_PROFILE --policy-document file://Policy.json | jq ".Policy.Arn" | tr -d '"') + +# Attach policy to role +aws iam attach-role-policy --role-name $ROLE_NAME --policy-arn $POLICY_ARN --region $AWS_REGION --profile $AWS_PROFILE + +# Clean up +rm Trust-Policy.json Policy.json +############################################ + +################### Layer ################## + +# Create OpenSSH layer +sudo docker run --rm -v $(pwd)/openssh-layer:/lambda/opt lambci/yumda:2 yum install -y openssh +cd openssh-layer +sudo zip -qry ./openssh-layer.zip . > /dev/null +LAYER_ARN=$(aws lambda publish-layer-version \ + --layer-name $LAYER_NAME \ + --zip-file fileb://openssh-layer.zip \ + --region $AWS_REGION \ + --profile $AWS_PROFILE \ + --query 'LayerVersionArn' \ + --output text) +cd .. + +# Clean up +sudo rm -r openssh-layer/ +############################################ + +################## Lambda ################## + +# Create lambda function +cd server +npm i +npm run build +cd dist && zip -qr ../lambda.zip . && cd .. +mv lambda.zip ../ +cd .. + +aws lambda create-function \ + --function-name $FUNCTION_NAME \ + --runtime nodejs18.x \ + --region $AWS_REGION \ + --profile $AWS_PROFILE \ + --handler index_lambda.handler \ + --zip-file fileb://lambda.zip \ + --layers $LAYER_ARN \ + --role $ROLE_ARN + +aws lambda add-permission \ + --function-name $FUNCTION_NAME \ + --action lambda:InvokeFunctionUrl \ + --principal "*" \ + --function-url-auth-type "NONE" \ + --statement-id url \ + --region $AWS_REGION \ + --profile $AWS_PROFILE + +FUNCTION_URL=$(aws lambda create-function-url-config --function-name "$FUNCTION_NAME" --auth-type "NONE" --region $AWS_REGION --profile $AWS_PROFILE | jq -r ".FunctionUrl") + +echo "CA deployed at URL:" +echo "${FUNCTION_URL}" + +# Clean up +rm -r server/node_modules/ server/package-lock.json lambda.zip +########################################### \ No newline at end of file diff --git a/private-ca/server/build.mjs b/private-ca/server/build.mjs new file mode 100644 index 0000000..036a195 --- /dev/null +++ b/private-ca/server/build.mjs @@ -0,0 +1,10 @@ +import { build } from 'esbuild'; + +await build({ + entryPoints: ['src/index_lambda.ts'], + bundle: true, + platform: 'node', + target: 'node18', + outfile: 'dist/index_lambda.js', + format: 'cjs', +}); \ No newline at end of file diff --git a/private-ca/server/package-lock.json b/private-ca/server/package-lock.json new file mode 100644 index 0000000..f5248e6 --- /dev/null +++ b/private-ca/server/package-lock.json @@ -0,0 +1,1888 @@ +{ + "name": "private-ca", + "version": "0.0.1", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "private-ca", + "version": "0.0.1", + "license": "ISC", + "dependencies": { + "@aws-sdk/client-ec2": "^3.827.0", + "@aws-sdk/client-secrets-manager": "^3.825.0", + "date-fns": "^4.1.0" + }, + "devDependencies": { + "@types/node": "^20.10.5", + "esbuild": "^0.25.5", + "typescript": "^5.3.3" + } + }, + "node_modules/@aws-crypto/sha256-browser": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/@aws-crypto/sha256-browser/-/sha256-browser-5.2.0.tgz", + "integrity": "sha512-AXfN/lGotSQwu6HNcEsIASo7kWXZ5HYWvfOmSNKDsEqC4OashTp8alTmaz+F7TC2L083SFv5RdB+qU3Vs1kZqw==", + "license": "Apache-2.0", + "dependencies": { + "@aws-crypto/sha256-js": "^5.2.0", + "@aws-crypto/supports-web-crypto": "^5.2.0", + "@aws-crypto/util": "^5.2.0", + "@aws-sdk/types": "^3.222.0", + "@aws-sdk/util-locate-window": "^3.0.0", + "@smithy/util-utf8": "^2.0.0", + "tslib": "^2.6.2" + } + }, + "node_modules/@aws-crypto/sha256-browser/node_modules/@smithy/is-array-buffer": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@smithy/is-array-buffer/-/is-array-buffer-2.2.0.tgz", + "integrity": "sha512-GGP3O9QFD24uGeAXYUjwSTXARoqpZykHadOmA8G5vfJPK0/DC67qa//0qvqrJzL1xc8WQWX7/yc7fwudjPHPhA==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@aws-crypto/sha256-browser/node_modules/@smithy/util-buffer-from": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@smithy/util-buffer-from/-/util-buffer-from-2.2.0.tgz", + "integrity": "sha512-IJdWBbTcMQ6DA0gdNhh/BwrLkDR+ADW5Kr1aZmd4k3DIF6ezMV4R2NIAmT08wQJ3yUK82thHWmC/TnK/wpMMIA==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/is-array-buffer": "^2.2.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@aws-crypto/sha256-browser/node_modules/@smithy/util-utf8": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/@smithy/util-utf8/-/util-utf8-2.3.0.tgz", + "integrity": "sha512-R8Rdn8Hy72KKcebgLiv8jQcQkXoLMOGGv5uI1/k0l+snqkOzQ1R0ChUBCxWMlBsFMekWjq0wRudIweFs7sKT5A==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/util-buffer-from": "^2.2.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@aws-crypto/sha256-js": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/@aws-crypto/sha256-js/-/sha256-js-5.2.0.tgz", + "integrity": "sha512-FFQQyu7edu4ufvIZ+OadFpHHOt+eSTBaYaki44c+akjg7qZg9oOQeLlk77F6tSYqjDAFClrHJk9tMf0HdVyOvA==", + "license": "Apache-2.0", + "dependencies": { + "@aws-crypto/util": "^5.2.0", + "@aws-sdk/types": "^3.222.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@aws-crypto/supports-web-crypto": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/@aws-crypto/supports-web-crypto/-/supports-web-crypto-5.2.0.tgz", + "integrity": "sha512-iAvUotm021kM33eCdNfwIN//F77/IADDSs58i+MDaOqFrVjZo9bAal0NK7HurRuWLLpF1iLX7gbWrjHjeo+YFg==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.6.2" + } + }, + "node_modules/@aws-crypto/util": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/@aws-crypto/util/-/util-5.2.0.tgz", + "integrity": "sha512-4RkU9EsI6ZpBve5fseQlGNUWKMa1RLPQ1dnjnQoe07ldfIzcsGb5hC5W0Dm7u423KWzawlrpbjXBrXCEv9zazQ==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "^3.222.0", + "@smithy/util-utf8": "^2.0.0", + "tslib": "^2.6.2" + } + }, + "node_modules/@aws-crypto/util/node_modules/@smithy/is-array-buffer": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@smithy/is-array-buffer/-/is-array-buffer-2.2.0.tgz", + "integrity": "sha512-GGP3O9QFD24uGeAXYUjwSTXARoqpZykHadOmA8G5vfJPK0/DC67qa//0qvqrJzL1xc8WQWX7/yc7fwudjPHPhA==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@aws-crypto/util/node_modules/@smithy/util-buffer-from": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@smithy/util-buffer-from/-/util-buffer-from-2.2.0.tgz", + "integrity": "sha512-IJdWBbTcMQ6DA0gdNhh/BwrLkDR+ADW5Kr1aZmd4k3DIF6ezMV4R2NIAmT08wQJ3yUK82thHWmC/TnK/wpMMIA==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/is-array-buffer": "^2.2.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@aws-crypto/util/node_modules/@smithy/util-utf8": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/@smithy/util-utf8/-/util-utf8-2.3.0.tgz", + "integrity": "sha512-R8Rdn8Hy72KKcebgLiv8jQcQkXoLMOGGv5uI1/k0l+snqkOzQ1R0ChUBCxWMlBsFMekWjq0wRudIweFs7sKT5A==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/util-buffer-from": "^2.2.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@aws-sdk/client-ec2": { + "version": "3.842.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/client-ec2/-/client-ec2-3.842.0.tgz", + "integrity": "sha512-TTQYEOaVYkVm+hvIMkfmbSJfS5739ya1pJV1XyBTceegzyAyUcchXh7x0AVY8X69QpOqKCksY3NQEXjby3n9gg==", + "license": "Apache-2.0", + "dependencies": { + "@aws-crypto/sha256-browser": "5.2.0", + "@aws-crypto/sha256-js": "5.2.0", + "@aws-sdk/core": "3.840.0", + "@aws-sdk/credential-provider-node": "3.840.0", + "@aws-sdk/middleware-host-header": "3.840.0", + "@aws-sdk/middleware-logger": "3.840.0", + "@aws-sdk/middleware-recursion-detection": "3.840.0", + "@aws-sdk/middleware-sdk-ec2": "3.840.0", + "@aws-sdk/middleware-user-agent": "3.840.0", + "@aws-sdk/region-config-resolver": "3.840.0", + "@aws-sdk/types": "3.840.0", + "@aws-sdk/util-endpoints": "3.840.0", + "@aws-sdk/util-user-agent-browser": "3.840.0", + "@aws-sdk/util-user-agent-node": "3.840.0", + "@smithy/config-resolver": "^4.1.4", + "@smithy/core": "^3.6.0", + "@smithy/fetch-http-handler": "^5.0.4", + "@smithy/hash-node": "^4.0.4", + "@smithy/invalid-dependency": "^4.0.4", + "@smithy/middleware-content-length": "^4.0.4", + "@smithy/middleware-endpoint": "^4.1.13", + "@smithy/middleware-retry": "^4.1.14", + "@smithy/middleware-serde": "^4.0.8", + "@smithy/middleware-stack": "^4.0.4", + "@smithy/node-config-provider": "^4.1.3", + "@smithy/node-http-handler": "^4.0.6", + "@smithy/protocol-http": "^5.1.2", + "@smithy/smithy-client": "^4.4.5", + "@smithy/types": "^4.3.1", + "@smithy/url-parser": "^4.0.4", + "@smithy/util-base64": "^4.0.0", + "@smithy/util-body-length-browser": "^4.0.0", + "@smithy/util-body-length-node": "^4.0.0", + "@smithy/util-defaults-mode-browser": "^4.0.21", + "@smithy/util-defaults-mode-node": "^4.0.21", + "@smithy/util-endpoints": "^3.0.6", + "@smithy/util-middleware": "^4.0.4", + "@smithy/util-retry": "^4.0.6", + "@smithy/util-utf8": "^4.0.0", + "@smithy/util-waiter": "^4.0.6", + "@types/uuid": "^9.0.1", + "tslib": "^2.6.2", + "uuid": "^9.0.1" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/client-secrets-manager": { + "version": "3.840.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/client-secrets-manager/-/client-secrets-manager-3.840.0.tgz", + "integrity": "sha512-oUcoZT4YJc/WUoxydfzSE3o89dBvdzan75XOLXg3JVg64os4ao8SUkIphR3YXmjmHz8qwaVNXVF4MpR3IxGPCg==", + "license": "Apache-2.0", + "dependencies": { + "@aws-crypto/sha256-browser": "5.2.0", + "@aws-crypto/sha256-js": "5.2.0", + "@aws-sdk/core": "3.840.0", + "@aws-sdk/credential-provider-node": "3.840.0", + "@aws-sdk/middleware-host-header": "3.840.0", + "@aws-sdk/middleware-logger": "3.840.0", + "@aws-sdk/middleware-recursion-detection": "3.840.0", + "@aws-sdk/middleware-user-agent": "3.840.0", + "@aws-sdk/region-config-resolver": "3.840.0", + "@aws-sdk/types": "3.840.0", + "@aws-sdk/util-endpoints": "3.840.0", + "@aws-sdk/util-user-agent-browser": "3.840.0", + "@aws-sdk/util-user-agent-node": "3.840.0", + "@smithy/config-resolver": "^4.1.4", + "@smithy/core": "^3.6.0", + "@smithy/fetch-http-handler": "^5.0.4", + "@smithy/hash-node": "^4.0.4", + "@smithy/invalid-dependency": "^4.0.4", + "@smithy/middleware-content-length": "^4.0.4", + "@smithy/middleware-endpoint": "^4.1.13", + "@smithy/middleware-retry": "^4.1.14", + "@smithy/middleware-serde": "^4.0.8", + "@smithy/middleware-stack": "^4.0.4", + "@smithy/node-config-provider": "^4.1.3", + "@smithy/node-http-handler": "^4.0.6", + "@smithy/protocol-http": "^5.1.2", + "@smithy/smithy-client": "^4.4.5", + "@smithy/types": "^4.3.1", + "@smithy/url-parser": "^4.0.4", + "@smithy/util-base64": "^4.0.0", + "@smithy/util-body-length-browser": "^4.0.0", + "@smithy/util-body-length-node": "^4.0.0", + "@smithy/util-defaults-mode-browser": "^4.0.21", + "@smithy/util-defaults-mode-node": "^4.0.21", + "@smithy/util-endpoints": "^3.0.6", + "@smithy/util-middleware": "^4.0.4", + "@smithy/util-retry": "^4.0.6", + "@smithy/util-utf8": "^4.0.0", + "@types/uuid": "^9.0.1", + "tslib": "^2.6.2", + "uuid": "^9.0.1" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/client-sso": { + "version": "3.840.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/client-sso/-/client-sso-3.840.0.tgz", + "integrity": "sha512-3Zp+FWN2hhmKdpS0Ragi5V2ZPsZNScE3jlbgoJjzjI/roHZqO+e3/+XFN4TlM0DsPKYJNp+1TAjmhxN6rOnfYA==", + "license": "Apache-2.0", + "dependencies": { + "@aws-crypto/sha256-browser": "5.2.0", + "@aws-crypto/sha256-js": "5.2.0", + "@aws-sdk/core": "3.840.0", + "@aws-sdk/middleware-host-header": "3.840.0", + "@aws-sdk/middleware-logger": "3.840.0", + "@aws-sdk/middleware-recursion-detection": "3.840.0", + "@aws-sdk/middleware-user-agent": "3.840.0", + "@aws-sdk/region-config-resolver": "3.840.0", + "@aws-sdk/types": "3.840.0", + "@aws-sdk/util-endpoints": "3.840.0", + "@aws-sdk/util-user-agent-browser": "3.840.0", + "@aws-sdk/util-user-agent-node": "3.840.0", + "@smithy/config-resolver": "^4.1.4", + "@smithy/core": "^3.6.0", + "@smithy/fetch-http-handler": "^5.0.4", + "@smithy/hash-node": "^4.0.4", + "@smithy/invalid-dependency": "^4.0.4", + "@smithy/middleware-content-length": "^4.0.4", + "@smithy/middleware-endpoint": "^4.1.13", + "@smithy/middleware-retry": "^4.1.14", + "@smithy/middleware-serde": "^4.0.8", + "@smithy/middleware-stack": "^4.0.4", + "@smithy/node-config-provider": "^4.1.3", + "@smithy/node-http-handler": "^4.0.6", + "@smithy/protocol-http": "^5.1.2", + "@smithy/smithy-client": "^4.4.5", + "@smithy/types": "^4.3.1", + "@smithy/url-parser": "^4.0.4", + "@smithy/util-base64": "^4.0.0", + "@smithy/util-body-length-browser": "^4.0.0", + "@smithy/util-body-length-node": "^4.0.0", + "@smithy/util-defaults-mode-browser": "^4.0.21", + "@smithy/util-defaults-mode-node": "^4.0.21", + "@smithy/util-endpoints": "^3.0.6", + "@smithy/util-middleware": "^4.0.4", + "@smithy/util-retry": "^4.0.6", + "@smithy/util-utf8": "^4.0.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/core": { + "version": "3.840.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/core/-/core-3.840.0.tgz", + "integrity": "sha512-x3Zgb39tF1h2XpU+yA4OAAQlW6LVEfXNlSedSYJ7HGKXqA/E9h3rWQVpYfhXXVVsLdYXdNw5KBUkoAoruoZSZA==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "3.840.0", + "@aws-sdk/xml-builder": "3.821.0", + "@smithy/core": "^3.6.0", + "@smithy/node-config-provider": "^4.1.3", + "@smithy/property-provider": "^4.0.4", + "@smithy/protocol-http": "^5.1.2", + "@smithy/signature-v4": "^5.1.2", + "@smithy/smithy-client": "^4.4.5", + "@smithy/types": "^4.3.1", + "@smithy/util-base64": "^4.0.0", + "@smithy/util-body-length-browser": "^4.0.0", + "@smithy/util-middleware": "^4.0.4", + "@smithy/util-utf8": "^4.0.0", + "fast-xml-parser": "4.4.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/credential-provider-env": { + "version": "3.840.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-env/-/credential-provider-env-3.840.0.tgz", + "integrity": "sha512-EzF6VcJK7XvQ/G15AVEfJzN2mNXU8fcVpXo4bRyr1S6t2q5zx6UPH/XjDbn18xyUmOq01t+r8gG+TmHEVo18fA==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/core": "3.840.0", + "@aws-sdk/types": "3.840.0", + "@smithy/property-provider": "^4.0.4", + "@smithy/types": "^4.3.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/credential-provider-http": { + "version": "3.840.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-http/-/credential-provider-http-3.840.0.tgz", + "integrity": "sha512-wbnUiPGLVea6mXbUh04fu+VJmGkQvmToPeTYdHE8eRZq3NRDi3t3WltT+jArLBKD/4NppRpMjf2ju4coMCz91g==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/core": "3.840.0", + "@aws-sdk/types": "3.840.0", + "@smithy/fetch-http-handler": "^5.0.4", + "@smithy/node-http-handler": "^4.0.6", + "@smithy/property-provider": "^4.0.4", + "@smithy/protocol-http": "^5.1.2", + "@smithy/smithy-client": "^4.4.5", + "@smithy/types": "^4.3.1", + "@smithy/util-stream": "^4.2.2", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/credential-provider-ini": { + "version": "3.840.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-ini/-/credential-provider-ini-3.840.0.tgz", + "integrity": "sha512-7F290BsWydShHb+7InXd+IjJc3mlEIm9I0R57F/Pjl1xZB69MdkhVGCnuETWoBt4g53ktJd6NEjzm/iAhFXFmw==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/core": "3.840.0", + "@aws-sdk/credential-provider-env": "3.840.0", + "@aws-sdk/credential-provider-http": "3.840.0", + "@aws-sdk/credential-provider-process": "3.840.0", + "@aws-sdk/credential-provider-sso": "3.840.0", + "@aws-sdk/credential-provider-web-identity": "3.840.0", + "@aws-sdk/nested-clients": "3.840.0", + "@aws-sdk/types": "3.840.0", + "@smithy/credential-provider-imds": "^4.0.6", + "@smithy/property-provider": "^4.0.4", + "@smithy/shared-ini-file-loader": "^4.0.4", + "@smithy/types": "^4.3.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/credential-provider-node": { + "version": "3.840.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-node/-/credential-provider-node-3.840.0.tgz", + "integrity": "sha512-KufP8JnxA31wxklLm63evUPSFApGcH8X86z3mv9SRbpCm5ycgWIGVCTXpTOdgq6rPZrwT9pftzv2/b4mV/9clg==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/credential-provider-env": "3.840.0", + "@aws-sdk/credential-provider-http": "3.840.0", + "@aws-sdk/credential-provider-ini": "3.840.0", + "@aws-sdk/credential-provider-process": "3.840.0", + "@aws-sdk/credential-provider-sso": "3.840.0", + "@aws-sdk/credential-provider-web-identity": "3.840.0", + "@aws-sdk/types": "3.840.0", + "@smithy/credential-provider-imds": "^4.0.6", + "@smithy/property-provider": "^4.0.4", + "@smithy/shared-ini-file-loader": "^4.0.4", + "@smithy/types": "^4.3.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/credential-provider-process": { + "version": "3.840.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-process/-/credential-provider-process-3.840.0.tgz", + "integrity": "sha512-HkDQWHy8tCI4A0Ps2NVtuVYMv9cB4y/IuD/TdOsqeRIAT12h8jDb98BwQPNLAImAOwOWzZJ8Cu0xtSpX7CQhMw==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/core": "3.840.0", + "@aws-sdk/types": "3.840.0", + "@smithy/property-provider": "^4.0.4", + "@smithy/shared-ini-file-loader": "^4.0.4", + "@smithy/types": "^4.3.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/credential-provider-sso": { + "version": "3.840.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-sso/-/credential-provider-sso-3.840.0.tgz", + "integrity": "sha512-2qgdtdd6R0Z1y0KL8gzzwFUGmhBHSUx4zy85L2XV1CXhpRNwV71SVWJqLDVV5RVWVf9mg50Pm3AWrUC0xb0pcA==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/client-sso": "3.840.0", + "@aws-sdk/core": "3.840.0", + "@aws-sdk/token-providers": "3.840.0", + "@aws-sdk/types": "3.840.0", + "@smithy/property-provider": "^4.0.4", + "@smithy/shared-ini-file-loader": "^4.0.4", + "@smithy/types": "^4.3.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/credential-provider-web-identity": { + "version": "3.840.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-web-identity/-/credential-provider-web-identity-3.840.0.tgz", + "integrity": "sha512-dpEeVXG8uNZSmVXReE4WP0lwoioX2gstk4RnUgrdUE3YaPq8A+hJiVAyc3h+cjDeIqfbsQbZm9qFetKC2LF9dQ==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/core": "3.840.0", + "@aws-sdk/nested-clients": "3.840.0", + "@aws-sdk/types": "3.840.0", + "@smithy/property-provider": "^4.0.4", + "@smithy/types": "^4.3.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/middleware-host-header": { + "version": "3.840.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-host-header/-/middleware-host-header-3.840.0.tgz", + "integrity": "sha512-ub+hXJAbAje94+Ya6c6eL7sYujoE8D4Bumu1NUI8TXjUhVVn0HzVWQjpRLshdLsUp1AW7XyeJaxyajRaJQ8+Xg==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "3.840.0", + "@smithy/protocol-http": "^5.1.2", + "@smithy/types": "^4.3.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/middleware-logger": { + "version": "3.840.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-logger/-/middleware-logger-3.840.0.tgz", + "integrity": "sha512-lSV8FvjpdllpGaRspywss4CtXV8M7NNNH+2/j86vMH+YCOZ6fu2T/TyFd/tHwZ92vDfHctWkRbQxg0bagqwovA==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "3.840.0", + "@smithy/types": "^4.3.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/middleware-recursion-detection": { + "version": "3.840.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-recursion-detection/-/middleware-recursion-detection-3.840.0.tgz", + "integrity": "sha512-Gu7lGDyfddyhIkj1Z1JtrY5NHb5+x/CRiB87GjaSrKxkDaydtX2CU977JIABtt69l9wLbcGDIQ+W0uJ5xPof7g==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "3.840.0", + "@smithy/protocol-http": "^5.1.2", + "@smithy/types": "^4.3.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/middleware-sdk-ec2": { + "version": "3.840.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-sdk-ec2/-/middleware-sdk-ec2-3.840.0.tgz", + "integrity": "sha512-TVYRq3NNq+Cb4N5jODASOmKwPBa4zXH0CT5Ifrav+fH7SVtkfXurVMkLaAu1zFHyllQgAQ6O4O/MpwDq2H1nkw==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "3.840.0", + "@aws-sdk/util-format-url": "3.840.0", + "@smithy/middleware-endpoint": "^4.1.13", + "@smithy/protocol-http": "^5.1.2", + "@smithy/signature-v4": "^5.1.2", + "@smithy/smithy-client": "^4.4.5", + "@smithy/types": "^4.3.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/middleware-user-agent": { + "version": "3.840.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-user-agent/-/middleware-user-agent-3.840.0.tgz", + "integrity": "sha512-hiiMf7BP5ZkAFAvWRcK67Mw/g55ar7OCrvrynC92hunx/xhMkrgSLM0EXIZ1oTn3uql9kH/qqGF0nqsK6K555A==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/core": "3.840.0", + "@aws-sdk/types": "3.840.0", + "@aws-sdk/util-endpoints": "3.840.0", + "@smithy/core": "^3.6.0", + "@smithy/protocol-http": "^5.1.2", + "@smithy/types": "^4.3.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/nested-clients": { + "version": "3.840.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/nested-clients/-/nested-clients-3.840.0.tgz", + "integrity": "sha512-LXYYo9+n4hRqnRSIMXLBb+BLz+cEmjMtTudwK1BF6Bn2RfdDv29KuyeDRrPCS3TwKl7ZKmXUmE9n5UuHAPfBpA==", + "license": "Apache-2.0", + "dependencies": { + "@aws-crypto/sha256-browser": "5.2.0", + "@aws-crypto/sha256-js": "5.2.0", + "@aws-sdk/core": "3.840.0", + "@aws-sdk/middleware-host-header": "3.840.0", + "@aws-sdk/middleware-logger": "3.840.0", + "@aws-sdk/middleware-recursion-detection": "3.840.0", + "@aws-sdk/middleware-user-agent": "3.840.0", + "@aws-sdk/region-config-resolver": "3.840.0", + "@aws-sdk/types": "3.840.0", + "@aws-sdk/util-endpoints": "3.840.0", + "@aws-sdk/util-user-agent-browser": "3.840.0", + "@aws-sdk/util-user-agent-node": "3.840.0", + "@smithy/config-resolver": "^4.1.4", + "@smithy/core": "^3.6.0", + "@smithy/fetch-http-handler": "^5.0.4", + "@smithy/hash-node": "^4.0.4", + "@smithy/invalid-dependency": "^4.0.4", + "@smithy/middleware-content-length": "^4.0.4", + "@smithy/middleware-endpoint": "^4.1.13", + "@smithy/middleware-retry": "^4.1.14", + "@smithy/middleware-serde": "^4.0.8", + "@smithy/middleware-stack": "^4.0.4", + "@smithy/node-config-provider": "^4.1.3", + "@smithy/node-http-handler": "^4.0.6", + "@smithy/protocol-http": "^5.1.2", + "@smithy/smithy-client": "^4.4.5", + "@smithy/types": "^4.3.1", + "@smithy/url-parser": "^4.0.4", + "@smithy/util-base64": "^4.0.0", + "@smithy/util-body-length-browser": "^4.0.0", + "@smithy/util-body-length-node": "^4.0.0", + "@smithy/util-defaults-mode-browser": "^4.0.21", + "@smithy/util-defaults-mode-node": "^4.0.21", + "@smithy/util-endpoints": "^3.0.6", + "@smithy/util-middleware": "^4.0.4", + "@smithy/util-retry": "^4.0.6", + "@smithy/util-utf8": "^4.0.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/region-config-resolver": { + "version": "3.840.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/region-config-resolver/-/region-config-resolver-3.840.0.tgz", + "integrity": "sha512-Qjnxd/yDv9KpIMWr90ZDPtRj0v75AqGC92Lm9+oHXZ8p1MjG5JE2CW0HL8JRgK9iKzgKBL7pPQRXI8FkvEVfrA==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "3.840.0", + "@smithy/node-config-provider": "^4.1.3", + "@smithy/types": "^4.3.1", + "@smithy/util-config-provider": "^4.0.0", + "@smithy/util-middleware": "^4.0.4", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/token-providers": { + "version": "3.840.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/token-providers/-/token-providers-3.840.0.tgz", + "integrity": "sha512-6BuTOLTXvmgwjK7ve7aTg9JaWFdM5UoMolLVPMyh3wTv9Ufalh8oklxYHUBIxsKkBGO2WiHXytveuxH6tAgTYg==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/core": "3.840.0", + "@aws-sdk/nested-clients": "3.840.0", + "@aws-sdk/types": "3.840.0", + "@smithy/property-provider": "^4.0.4", + "@smithy/shared-ini-file-loader": "^4.0.4", + "@smithy/types": "^4.3.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/types": { + "version": "3.840.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/types/-/types-3.840.0.tgz", + "integrity": "sha512-xliuHaUFZxEx1NSXeLLZ9Dyu6+EJVQKEoD+yM+zqUo3YDZ7medKJWY6fIOKiPX/N7XbLdBYwajb15Q7IL8KkeA==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^4.3.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/util-endpoints": { + "version": "3.840.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/util-endpoints/-/util-endpoints-3.840.0.tgz", + "integrity": "sha512-eqE9ROdg/Kk0rj3poutyRCFauPDXIf/WSvCqFiRDDVi6QOnCv/M0g2XW8/jSvkJlOyaXkNCptapIp6BeeFFGYw==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "3.840.0", + "@smithy/types": "^4.3.1", + "@smithy/util-endpoints": "^3.0.6", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/util-format-url": { + "version": "3.840.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/util-format-url/-/util-format-url-3.840.0.tgz", + "integrity": "sha512-VB1PWyI1TQPiPvg4w7tgUGGQER1xxXPNUqfh3baxUSFi1Oh8wHrDnFywkxLm3NMmgDmnLnSZ5Q326qAoyqKLSg==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "3.840.0", + "@smithy/querystring-builder": "^4.0.4", + "@smithy/types": "^4.3.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/util-locate-window": { + "version": "3.804.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/util-locate-window/-/util-locate-window-3.804.0.tgz", + "integrity": "sha512-zVoRfpmBVPodYlnMjgVjfGoEZagyRF5IPn3Uo6ZvOZp24chnW/FRstH7ESDHDDRga4z3V+ElUQHKpFDXWyBW5A==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/util-user-agent-browser": { + "version": "3.840.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/util-user-agent-browser/-/util-user-agent-browser-3.840.0.tgz", + "integrity": "sha512-JdyZM3EhhL4PqwFpttZu1afDpPJCCc3eyZOLi+srpX11LsGj6sThf47TYQN75HT1CarZ7cCdQHGzP2uy3/xHfQ==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "3.840.0", + "@smithy/types": "^4.3.1", + "bowser": "^2.11.0", + "tslib": "^2.6.2" + } + }, + "node_modules/@aws-sdk/util-user-agent-node": { + "version": "3.840.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/util-user-agent-node/-/util-user-agent-node-3.840.0.tgz", + "integrity": "sha512-Fy5JUEDQU1tPm2Yw/YqRYYc27W5+QD/J4mYvQvdWjUGZLB5q3eLFMGD35Uc28ZFoGMufPr4OCxK/bRfWROBRHQ==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/middleware-user-agent": "3.840.0", + "@aws-sdk/types": "3.840.0", + "@smithy/node-config-provider": "^4.1.3", + "@smithy/types": "^4.3.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + }, + "peerDependencies": { + "aws-crt": ">=1.0.0" + }, + "peerDependenciesMeta": { + "aws-crt": { + "optional": true + } + } + }, + "node_modules/@aws-sdk/xml-builder": { + "version": "3.821.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/xml-builder/-/xml-builder-3.821.0.tgz", + "integrity": "sha512-DIIotRnefVL6DiaHtO6/21DhJ4JZnnIwdNbpwiAhdt/AVbttcE4yw925gsjur0OGv5BTYXQXU3YnANBYnZjuQA==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^4.3.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.25.5", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.5.tgz", + "integrity": "sha512-9o3TMmpmftaCMepOdA5k/yDw8SfInyzWWTjYTFCX3kPSDJMROQTb8jg+h9Cnwnmm1vOzvxN7gIfB5V2ewpjtGA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.25.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.5.tgz", + "integrity": "sha512-AdJKSPeEHgi7/ZhuIPtcQKr5RQdo6OO2IL87JkianiMYMPbCtot9fxPbrMiBADOWWm3T2si9stAiVsGbTQFkbA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.25.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.5.tgz", + "integrity": "sha512-VGzGhj4lJO+TVGV1v8ntCZWJktV7SGCs3Pn1GRWI1SBFtRALoomm8k5E9Pmwg3HOAal2VDc2F9+PM/rEY6oIDg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.25.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.5.tgz", + "integrity": "sha512-D2GyJT1kjvO//drbRT3Hib9XPwQeWd9vZoBJn+bu/lVsOZ13cqNdDeqIF/xQ5/VmWvMduP6AmXvylO/PIc2isw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.25.5", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.5.tgz", + "integrity": "sha512-GtaBgammVvdF7aPIgH2jxMDdivezgFu6iKpmT+48+F8Hhg5J/sfnDieg0aeG/jfSvkYQU2/pceFPDKlqZzwnfQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.25.5", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.5.tgz", + "integrity": "sha512-1iT4FVL0dJ76/q1wd7XDsXrSW+oLoquptvh4CLR4kITDtqi2e/xwXwdCVH8hVHU43wgJdsq7Gxuzcs6Iq/7bxQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.25.5", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.5.tgz", + "integrity": "sha512-nk4tGP3JThz4La38Uy/gzyXtpkPW8zSAmoUhK9xKKXdBCzKODMc2adkB2+8om9BDYugz+uGV7sLmpTYzvmz6Sw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.25.5", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.5.tgz", + "integrity": "sha512-PrikaNjiXdR2laW6OIjlbeuCPrPaAl0IwPIaRv+SMV8CiM8i2LqVUHFC1+8eORgWyY7yhQY+2U2fA55mBzReaw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.25.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.5.tgz", + "integrity": "sha512-cPzojwW2okgh7ZlRpcBEtsX7WBuqbLrNXqLU89GxWbNt6uIg78ET82qifUy3W6OVww6ZWobWub5oqZOVtwolfw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.25.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.5.tgz", + "integrity": "sha512-Z9kfb1v6ZlGbWj8EJk9T6czVEjjq2ntSYLY2cw6pAZl4oKtfgQuS4HOq41M/BcoLPzrUbNd+R4BXFyH//nHxVg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.25.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.5.tgz", + "integrity": "sha512-sQ7l00M8bSv36GLV95BVAdhJ2QsIbCuCjh/uYrWiMQSUuV+LpXwIqhgJDcvMTj+VsQmqAHL2yYaasENvJ7CDKA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.25.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.5.tgz", + "integrity": "sha512-0ur7ae16hDUC4OL5iEnDb0tZHDxYmuQyhKhsPBV8f99f6Z9KQM02g33f93rNH5A30agMS46u2HP6qTdEt6Q1kg==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.25.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.5.tgz", + "integrity": "sha512-kB/66P1OsHO5zLz0i6X0RxlQ+3cu0mkxS3TKFvkb5lin6uwZ/ttOkP3Z8lfR9mJOBk14ZwZ9182SIIWFGNmqmg==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.25.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.5.tgz", + "integrity": "sha512-UZCmJ7r9X2fe2D6jBmkLBMQetXPXIsZjQJCjgwpVDz+YMcS6oFR27alkgGv3Oqkv07bxdvw7fyB71/olceJhkQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.25.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.5.tgz", + "integrity": "sha512-kTxwu4mLyeOlsVIFPfQo+fQJAV9mh24xL+y+Bm6ej067sYANjyEw1dNHmvoqxJUCMnkBdKpvOn0Ahql6+4VyeA==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.25.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.5.tgz", + "integrity": "sha512-K2dSKTKfmdh78uJ3NcWFiqyRrimfdinS5ErLSn3vluHNeHVnBAFWC8a4X5N+7FgVE1EjXS1QDZbpqZBjfrqMTQ==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.25.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.5.tgz", + "integrity": "sha512-uhj8N2obKTE6pSZ+aMUbqq+1nXxNjZIIjCjGLfsWvVpy7gKCOL6rsY1MhRh9zLtUtAI7vpgLMK6DxjO8Qm9lJw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-arm64": { + "version": "0.25.5", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.5.tgz", + "integrity": "sha512-pwHtMP9viAy1oHPvgxtOv+OkduK5ugofNTVDilIzBLpoWAM16r7b/mxBvfpuQDpRQFMfuVr5aLcn4yveGvBZvw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.25.5", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.5.tgz", + "integrity": "sha512-WOb5fKrvVTRMfWFNCroYWWklbnXH0Q5rZppjq0vQIdlsQKuw6mdSihwSo4RV/YdQ5UCKKvBy7/0ZZYLBZKIbwQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.25.5", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.5.tgz", + "integrity": "sha512-7A208+uQKgTxHd0G0uqZO8UjK2R0DDb4fDmERtARjSHWxqMTye4Erz4zZafx7Di9Cv+lNHYuncAkiGFySoD+Mw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.25.5", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.5.tgz", + "integrity": "sha512-G4hE405ErTWraiZ8UiSoesH8DaCsMm0Cay4fsFWOOUcz8b8rC6uCvnagr+gnioEjWn0wC+o1/TAHt+It+MpIMg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.25.5", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.5.tgz", + "integrity": "sha512-l+azKShMy7FxzY0Rj4RCt5VD/q8mG/e+mDivgspo+yL8zW7qEwctQ6YqKX34DTEleFAvCIUviCFX1SDZRSyMQA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.25.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.5.tgz", + "integrity": "sha512-O2S7SNZzdcFG7eFKgvwUEZ2VG9D/sn/eIiz8XRZ1Q/DO5a3s76Xv0mdBzVM5j5R639lXQmPmSo0iRpHqUUrsxw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.25.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.5.tgz", + "integrity": "sha512-onOJ02pqs9h1iMJ1PQphR+VZv8qBMQ77Klcsqv9CNW2w6yLqoURLcgERAIurY6QE63bbLuqgP9ATqajFLK5AMQ==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.25.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.5.tgz", + "integrity": "sha512-TXv6YnJ8ZMVdX+SXWVBo/0p8LTcrUYngpWjvm91TMjjBQii7Oz11Lw5lbDV5Y0TzuhSJHwiH4hEtC1I42mMS0g==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@smithy/abort-controller": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/@smithy/abort-controller/-/abort-controller-4.0.4.tgz", + "integrity": "sha512-gJnEjZMvigPDQWHrW3oPrFhQtkrgqBkyjj3pCIdF3A5M6vsZODG93KNlfJprv6bp4245bdT32fsHK4kkH3KYDA==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^4.3.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/config-resolver": { + "version": "4.1.4", + "resolved": "https://registry.npmjs.org/@smithy/config-resolver/-/config-resolver-4.1.4.tgz", + "integrity": "sha512-prmU+rDddxHOH0oNcwemL+SwnzcG65sBF2yXRO7aeXIn/xTlq2pX7JLVbkBnVLowHLg4/OL4+jBmv9hVrVGS+w==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/node-config-provider": "^4.1.3", + "@smithy/types": "^4.3.1", + "@smithy/util-config-provider": "^4.0.0", + "@smithy/util-middleware": "^4.0.4", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/core": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/@smithy/core/-/core-3.6.0.tgz", + "integrity": "sha512-Pgvfb+TQ4wUNLyHzvgCP4aYZMh16y7GcfF59oirRHcgGgkH1e/s9C0nv/v3WP+Quymyr5je71HeFQCwh+44XLg==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/middleware-serde": "^4.0.8", + "@smithy/protocol-http": "^5.1.2", + "@smithy/types": "^4.3.1", + "@smithy/util-base64": "^4.0.0", + "@smithy/util-body-length-browser": "^4.0.0", + "@smithy/util-middleware": "^4.0.4", + "@smithy/util-stream": "^4.2.2", + "@smithy/util-utf8": "^4.0.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/credential-provider-imds": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/@smithy/credential-provider-imds/-/credential-provider-imds-4.0.6.tgz", + "integrity": "sha512-hKMWcANhUiNbCJouYkZ9V3+/Qf9pteR1dnwgdyzR09R4ODEYx8BbUysHwRSyex4rZ9zapddZhLFTnT4ZijR4pw==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/node-config-provider": "^4.1.3", + "@smithy/property-provider": "^4.0.4", + "@smithy/types": "^4.3.1", + "@smithy/url-parser": "^4.0.4", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/fetch-http-handler": { + "version": "5.0.4", + "resolved": "https://registry.npmjs.org/@smithy/fetch-http-handler/-/fetch-http-handler-5.0.4.tgz", + "integrity": "sha512-AMtBR5pHppYMVD7z7G+OlHHAcgAN7v0kVKEpHuTO4Gb199Gowh0taYi9oDStFeUhetkeP55JLSVlTW1n9rFtUw==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/protocol-http": "^5.1.2", + "@smithy/querystring-builder": "^4.0.4", + "@smithy/types": "^4.3.1", + "@smithy/util-base64": "^4.0.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/hash-node": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/@smithy/hash-node/-/hash-node-4.0.4.tgz", + "integrity": "sha512-qnbTPUhCVnCgBp4z4BUJUhOEkVwxiEi1cyFM+Zj6o+aY8OFGxUQleKWq8ltgp3dujuhXojIvJWdoqpm6dVO3lQ==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^4.3.1", + "@smithy/util-buffer-from": "^4.0.0", + "@smithy/util-utf8": "^4.0.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/invalid-dependency": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/@smithy/invalid-dependency/-/invalid-dependency-4.0.4.tgz", + "integrity": "sha512-bNYMi7WKTJHu0gn26wg8OscncTt1t2b8KcsZxvOv56XA6cyXtOAAAaNP7+m45xfppXfOatXF3Sb1MNsLUgVLTw==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^4.3.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/is-array-buffer": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@smithy/is-array-buffer/-/is-array-buffer-4.0.0.tgz", + "integrity": "sha512-saYhF8ZZNoJDTvJBEWgeBccCg+yvp1CX+ed12yORU3NilJScfc6gfch2oVb4QgxZrGUx3/ZJlb+c/dJbyupxlw==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/middleware-content-length": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/@smithy/middleware-content-length/-/middleware-content-length-4.0.4.tgz", + "integrity": "sha512-F7gDyfI2BB1Kc+4M6rpuOLne5LOcEknH1n6UQB69qv+HucXBR1rkzXBnQTB2q46sFy1PM/zuSJOB532yc8bg3w==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/protocol-http": "^5.1.2", + "@smithy/types": "^4.3.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/middleware-endpoint": { + "version": "4.1.13", + "resolved": "https://registry.npmjs.org/@smithy/middleware-endpoint/-/middleware-endpoint-4.1.13.tgz", + "integrity": "sha512-xg3EHV/Q5ZdAO5b0UiIMj3RIOCobuS40pBBODguUDVdko6YK6QIzCVRrHTogVuEKglBWqWenRnZ71iZnLL3ZAQ==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/core": "^3.6.0", + "@smithy/middleware-serde": "^4.0.8", + "@smithy/node-config-provider": "^4.1.3", + "@smithy/shared-ini-file-loader": "^4.0.4", + "@smithy/types": "^4.3.1", + "@smithy/url-parser": "^4.0.4", + "@smithy/util-middleware": "^4.0.4", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/middleware-retry": { + "version": "4.1.14", + "resolved": "https://registry.npmjs.org/@smithy/middleware-retry/-/middleware-retry-4.1.14.tgz", + "integrity": "sha512-eoXaLlDGpKvdmvt+YBfRXE7HmIEtFF+DJCbTPwuLunP0YUnrydl+C4tS+vEM0+nyxXrX3PSUFqC+lP1+EHB1Tw==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/node-config-provider": "^4.1.3", + "@smithy/protocol-http": "^5.1.2", + "@smithy/service-error-classification": "^4.0.6", + "@smithy/smithy-client": "^4.4.5", + "@smithy/types": "^4.3.1", + "@smithy/util-middleware": "^4.0.4", + "@smithy/util-retry": "^4.0.6", + "tslib": "^2.6.2", + "uuid": "^9.0.1" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/middleware-serde": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/@smithy/middleware-serde/-/middleware-serde-4.0.8.tgz", + "integrity": "sha512-iSSl7HJoJaGyMIoNn2B7czghOVwJ9nD7TMvLhMWeSB5vt0TnEYyRRqPJu/TqW76WScaNvYYB8nRoiBHR9S1Ddw==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/protocol-http": "^5.1.2", + "@smithy/types": "^4.3.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/middleware-stack": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/@smithy/middleware-stack/-/middleware-stack-4.0.4.tgz", + "integrity": "sha512-kagK5ggDrBUCCzI93ft6DjteNSfY8Ulr83UtySog/h09lTIOAJ/xUSObutanlPT0nhoHAkpmW9V5K8oPyLh+QA==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^4.3.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/node-config-provider": { + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/@smithy/node-config-provider/-/node-config-provider-4.1.3.tgz", + "integrity": "sha512-HGHQr2s59qaU1lrVH6MbLlmOBxadtzTsoO4c+bF5asdgVik3I8o7JIOzoeqWc5MjVa+vD36/LWE0iXKpNqooRw==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/property-provider": "^4.0.4", + "@smithy/shared-ini-file-loader": "^4.0.4", + "@smithy/types": "^4.3.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/node-http-handler": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/@smithy/node-http-handler/-/node-http-handler-4.0.6.tgz", + "integrity": "sha512-NqbmSz7AW2rvw4kXhKGrYTiJVDHnMsFnX4i+/FzcZAfbOBauPYs2ekuECkSbtqaxETLLTu9Rl/ex6+I2BKErPA==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/abort-controller": "^4.0.4", + "@smithy/protocol-http": "^5.1.2", + "@smithy/querystring-builder": "^4.0.4", + "@smithy/types": "^4.3.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/property-provider": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/@smithy/property-provider/-/property-provider-4.0.4.tgz", + "integrity": "sha512-qHJ2sSgu4FqF4U/5UUp4DhXNmdTrgmoAai6oQiM+c5RZ/sbDwJ12qxB1M6FnP+Tn/ggkPZf9ccn4jqKSINaquw==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^4.3.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/protocol-http": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/@smithy/protocol-http/-/protocol-http-5.1.2.tgz", + "integrity": "sha512-rOG5cNLBXovxIrICSBm95dLqzfvxjEmuZx4KK3hWwPFHGdW3lxY0fZNXfv2zebfRO7sJZ5pKJYHScsqopeIWtQ==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^4.3.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/querystring-builder": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/@smithy/querystring-builder/-/querystring-builder-4.0.4.tgz", + "integrity": "sha512-SwREZcDnEYoh9tLNgMbpop+UTGq44Hl9tdj3rf+yeLcfH7+J8OXEBaMc2kDxtyRHu8BhSg9ADEx0gFHvpJgU8w==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^4.3.1", + "@smithy/util-uri-escape": "^4.0.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/querystring-parser": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/@smithy/querystring-parser/-/querystring-parser-4.0.4.tgz", + "integrity": "sha512-6yZf53i/qB8gRHH/l2ZwUG5xgkPgQF15/KxH0DdXMDHjesA9MeZje/853ifkSY0x4m5S+dfDZ+c4x439PF0M2w==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^4.3.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/service-error-classification": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/@smithy/service-error-classification/-/service-error-classification-4.0.6.tgz", + "integrity": "sha512-RRoTDL//7xi4tn5FrN2NzH17jbgmnKidUqd4KvquT0954/i6CXXkh1884jBiunq24g9cGtPBEXlU40W6EpNOOg==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^4.3.1" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/shared-ini-file-loader": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/@smithy/shared-ini-file-loader/-/shared-ini-file-loader-4.0.4.tgz", + "integrity": "sha512-63X0260LoFBjrHifPDs+nM9tV0VMkOTl4JRMYNuKh/f5PauSjowTfvF3LogfkWdcPoxsA9UjqEOgjeYIbhb7Nw==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^4.3.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/signature-v4": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/@smithy/signature-v4/-/signature-v4-5.1.2.tgz", + "integrity": "sha512-d3+U/VpX7a60seHziWnVZOHuEgJlclufjkS6zhXvxcJgkJq4UWdH5eOBLzHRMx6gXjsdT9h6lfpmLzbrdupHgQ==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/is-array-buffer": "^4.0.0", + "@smithy/protocol-http": "^5.1.2", + "@smithy/types": "^4.3.1", + "@smithy/util-hex-encoding": "^4.0.0", + "@smithy/util-middleware": "^4.0.4", + "@smithy/util-uri-escape": "^4.0.0", + "@smithy/util-utf8": "^4.0.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/smithy-client": { + "version": "4.4.5", + "resolved": "https://registry.npmjs.org/@smithy/smithy-client/-/smithy-client-4.4.5.tgz", + "integrity": "sha512-+lynZjGuUFJaMdDYSTMnP/uPBBXXukVfrJlP+1U/Dp5SFTEI++w6NMga8DjOENxecOF71V9Z2DllaVDYRnGlkg==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/core": "^3.6.0", + "@smithy/middleware-endpoint": "^4.1.13", + "@smithy/middleware-stack": "^4.0.4", + "@smithy/protocol-http": "^5.1.2", + "@smithy/types": "^4.3.1", + "@smithy/util-stream": "^4.2.2", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/types": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/@smithy/types/-/types-4.3.1.tgz", + "integrity": "sha512-UqKOQBL2x6+HWl3P+3QqFD4ncKq0I8Nuz9QItGv5WuKuMHuuwlhvqcZCoXGfc+P1QmfJE7VieykoYYmrOoFJxA==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/url-parser": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/@smithy/url-parser/-/url-parser-4.0.4.tgz", + "integrity": "sha512-eMkc144MuN7B0TDA4U2fKs+BqczVbk3W+qIvcoCY6D1JY3hnAdCuhCZODC+GAeaxj0p6Jroz4+XMUn3PCxQQeQ==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/querystring-parser": "^4.0.4", + "@smithy/types": "^4.3.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/util-base64": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@smithy/util-base64/-/util-base64-4.0.0.tgz", + "integrity": "sha512-CvHfCmO2mchox9kjrtzoHkWHxjHZzaFojLc8quxXY7WAAMAg43nuxwv95tATVgQFNDwd4M9S1qFzj40Ul41Kmg==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/util-buffer-from": "^4.0.0", + "@smithy/util-utf8": "^4.0.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/util-body-length-browser": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@smithy/util-body-length-browser/-/util-body-length-browser-4.0.0.tgz", + "integrity": "sha512-sNi3DL0/k64/LO3A256M+m3CDdG6V7WKWHdAiBBMUN8S3hK3aMPhwnPik2A/a2ONN+9doY9UxaLfgqsIRg69QA==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/util-body-length-node": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@smithy/util-body-length-node/-/util-body-length-node-4.0.0.tgz", + "integrity": "sha512-q0iDP3VsZzqJyje8xJWEJCNIu3lktUGVoSy1KB0UWym2CL1siV3artm+u1DFYTLejpsrdGyCSWBdGNjJzfDPjg==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/util-buffer-from": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@smithy/util-buffer-from/-/util-buffer-from-4.0.0.tgz", + "integrity": "sha512-9TOQ7781sZvddgO8nxueKi3+yGvkY35kotA0Y6BWRajAv8jjmigQ1sBwz0UX47pQMYXJPahSKEKYFgt+rXdcug==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/is-array-buffer": "^4.0.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/util-config-provider": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@smithy/util-config-provider/-/util-config-provider-4.0.0.tgz", + "integrity": "sha512-L1RBVzLyfE8OXH+1hsJ8p+acNUSirQnWQ6/EgpchV88G6zGBTDPdXiiExei6Z1wR2RxYvxY/XLw6AMNCCt8H3w==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/util-defaults-mode-browser": { + "version": "4.0.21", + "resolved": "https://registry.npmjs.org/@smithy/util-defaults-mode-browser/-/util-defaults-mode-browser-4.0.21.tgz", + "integrity": "sha512-wM0jhTytgXu3wzJoIqpbBAG5U6BwiubZ6QKzSbP7/VbmF1v96xlAbX2Am/mz0Zep0NLvLh84JT0tuZnk3wmYQA==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/property-provider": "^4.0.4", + "@smithy/smithy-client": "^4.4.5", + "@smithy/types": "^4.3.1", + "bowser": "^2.11.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/util-defaults-mode-node": { + "version": "4.0.21", + "resolved": "https://registry.npmjs.org/@smithy/util-defaults-mode-node/-/util-defaults-mode-node-4.0.21.tgz", + "integrity": "sha512-/F34zkoU0GzpUgLJydHY8Rxu9lBn8xQC/s/0M0U9lLBkYbA1htaAFjWYJzpzsbXPuri5D1H8gjp2jBum05qBrA==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/config-resolver": "^4.1.4", + "@smithy/credential-provider-imds": "^4.0.6", + "@smithy/node-config-provider": "^4.1.3", + "@smithy/property-provider": "^4.0.4", + "@smithy/smithy-client": "^4.4.5", + "@smithy/types": "^4.3.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/util-endpoints": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/@smithy/util-endpoints/-/util-endpoints-3.0.6.tgz", + "integrity": "sha512-YARl3tFL3WgPuLzljRUnrS2ngLiUtkwhQtj8PAL13XZSyUiNLQxwG3fBBq3QXFqGFUXepIN73pINp3y8c2nBmA==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/node-config-provider": "^4.1.3", + "@smithy/types": "^4.3.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/util-hex-encoding": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@smithy/util-hex-encoding/-/util-hex-encoding-4.0.0.tgz", + "integrity": "sha512-Yk5mLhHtfIgW2W2WQZWSg5kuMZCVbvhFmC7rV4IO2QqnZdbEFPmQnCcGMAX2z/8Qj3B9hYYNjZOhWym+RwhePw==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/util-middleware": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/@smithy/util-middleware/-/util-middleware-4.0.4.tgz", + "integrity": "sha512-9MLKmkBmf4PRb0ONJikCbCwORACcil6gUWojwARCClT7RmLzF04hUR4WdRprIXal7XVyrddadYNfp2eF3nrvtQ==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^4.3.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/util-retry": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/@smithy/util-retry/-/util-retry-4.0.6.tgz", + "integrity": "sha512-+YekoF2CaSMv6zKrA6iI/N9yva3Gzn4L6n35Luydweu5MMPYpiGZlWqehPHDHyNbnyaYlz/WJyYAZnC+loBDZg==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/service-error-classification": "^4.0.6", + "@smithy/types": "^4.3.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/util-stream": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/@smithy/util-stream/-/util-stream-4.2.2.tgz", + "integrity": "sha512-aI+GLi7MJoVxg24/3J1ipwLoYzgkB4kUfogZfnslcYlynj3xsQ0e7vk4TnTro9hhsS5PvX1mwmkRqqHQjwcU7w==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/fetch-http-handler": "^5.0.4", + "@smithy/node-http-handler": "^4.0.6", + "@smithy/types": "^4.3.1", + "@smithy/util-base64": "^4.0.0", + "@smithy/util-buffer-from": "^4.0.0", + "@smithy/util-hex-encoding": "^4.0.0", + "@smithy/util-utf8": "^4.0.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/util-uri-escape": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@smithy/util-uri-escape/-/util-uri-escape-4.0.0.tgz", + "integrity": "sha512-77yfbCbQMtgtTylO9itEAdpPXSog3ZxMe09AEhm0dU0NLTalV70ghDZFR+Nfi1C60jnJoh/Re4090/DuZh2Omg==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/util-utf8": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@smithy/util-utf8/-/util-utf8-4.0.0.tgz", + "integrity": "sha512-b+zebfKCfRdgNJDknHCob3O7FpeYQN6ZG6YLExMcasDHsCXlsXCEuiPZeLnJLpwa5dvPetGlnGCiMHuLwGvFow==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/util-buffer-from": "^4.0.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/util-waiter": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/@smithy/util-waiter/-/util-waiter-4.0.6.tgz", + "integrity": "sha512-slcr1wdRbX7NFphXZOxtxRNA7hXAAtJAXJDE/wdoMAos27SIquVCKiSqfB6/28YzQ8FCsB5NKkhdM5gMADbqxg==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/abort-controller": "^4.0.4", + "@smithy/types": "^4.3.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@types/node": { + "version": "20.19.4", + "resolved": "https://registry.npmjs.org/@types/node/-/node-20.19.4.tgz", + "integrity": "sha512-OP+We5WV8Xnbuvw0zC2m4qfB/BJvjyCwtNjhHdJxV1639SGSKrLmJkc3fMnp2Qy8nJyHp8RO6umxELN/dS1/EA==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~6.21.0" + } + }, + "node_modules/@types/uuid": { + "version": "9.0.8", + "resolved": "https://registry.npmjs.org/@types/uuid/-/uuid-9.0.8.tgz", + "integrity": "sha512-jg+97EGIcY9AGHJJRaaPVgetKDsrTgbRjQ5Msgjh/DQKEFl0DtyRr/VCOyD1T2R1MNeWPK/u7JoGhlDZnKBAfA==", + "license": "MIT" + }, + "node_modules/bowser": { + "version": "2.11.0", + "resolved": "https://registry.npmjs.org/bowser/-/bowser-2.11.0.tgz", + "integrity": "sha512-AlcaJBi/pqqJBIQ8U9Mcpc9i8Aqxn88Skv5d+xBX006BY5u8N3mGLHa5Lgppa7L/HfwgwLgZ6NYs+Ag6uUmJRA==", + "license": "MIT" + }, + "node_modules/date-fns": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/date-fns/-/date-fns-4.1.0.tgz", + "integrity": "sha512-Ukq0owbQXxa/U3EGtsdVBkR1w7KOQ5gIBqdH2hkvknzZPYvBxb/aa6E8L7tmjFtkwZBu3UXBbjIgPo/Ez4xaNg==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/kossnocorp" + } + }, + "node_modules/esbuild": { + "version": "0.25.5", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.5.tgz", + "integrity": "sha512-P8OtKZRv/5J5hhz0cUAdu/cLuPIKXpQl1R9pZtvmHWQvrAUVd0UNIPT4IB4W3rNOqVO0rlqHmCIbSwxh/c9yUQ==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.25.5", + "@esbuild/android-arm": "0.25.5", + "@esbuild/android-arm64": "0.25.5", + "@esbuild/android-x64": "0.25.5", + "@esbuild/darwin-arm64": "0.25.5", + "@esbuild/darwin-x64": "0.25.5", + "@esbuild/freebsd-arm64": "0.25.5", + "@esbuild/freebsd-x64": "0.25.5", + "@esbuild/linux-arm": "0.25.5", + "@esbuild/linux-arm64": "0.25.5", + "@esbuild/linux-ia32": "0.25.5", + "@esbuild/linux-loong64": "0.25.5", + "@esbuild/linux-mips64el": "0.25.5", + "@esbuild/linux-ppc64": "0.25.5", + "@esbuild/linux-riscv64": "0.25.5", + "@esbuild/linux-s390x": "0.25.5", + "@esbuild/linux-x64": "0.25.5", + "@esbuild/netbsd-arm64": "0.25.5", + "@esbuild/netbsd-x64": "0.25.5", + "@esbuild/openbsd-arm64": "0.25.5", + "@esbuild/openbsd-x64": "0.25.5", + "@esbuild/sunos-x64": "0.25.5", + "@esbuild/win32-arm64": "0.25.5", + "@esbuild/win32-ia32": "0.25.5", + "@esbuild/win32-x64": "0.25.5" + } + }, + "node_modules/fast-xml-parser": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/fast-xml-parser/-/fast-xml-parser-4.4.1.tgz", + "integrity": "sha512-xkjOecfnKGkSsOwtZ5Pz7Us/T6mrbPQrq0nh+aCO5V9nk5NLWmasAHumTKjiPJPWANe+kAZ84Jc8ooJkzZ88Sw==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/NaturalIntelligence" + }, + { + "type": "paypal", + "url": "https://paypal.me/naturalintelligence" + } + ], + "license": "MIT", + "dependencies": { + "strnum": "^1.0.5" + }, + "bin": { + "fxparser": "src/cli/cli.js" + } + }, + "node_modules/strnum": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/strnum/-/strnum-1.1.2.tgz", + "integrity": "sha512-vrN+B7DBIoTTZjnPNewwhx6cBA/H+IS7rfW68n7XxC1y7uoiGQBxaKzqucGUgavX15dJgiGztLJ8vxuEzwqBdA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/NaturalIntelligence" + } + ], + "license": "MIT" + }, + "node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "license": "0BSD" + }, + "node_modules/typescript": { + "version": "5.8.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.8.3.tgz", + "integrity": "sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/undici-types": { + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", + "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/uuid": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-9.0.1.tgz", + "integrity": "sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA==", + "funding": [ + "https://github.com/sponsors/broofa", + "https://github.com/sponsors/ctavan" + ], + "license": "MIT", + "bin": { + "uuid": "dist/bin/uuid" + } + } + } +} diff --git a/private-ca/server/package.json b/private-ca/server/package.json new file mode 100644 index 0000000..cf1cbd2 --- /dev/null +++ b/private-ca/server/package.json @@ -0,0 +1,24 @@ +{ + "name": "private-ca", + "version": "0.0.1", + "description": "", + "type": "module", + "main": "dist/index_lambda.js", + "scripts": { + "start": "node dist/index_lambda.js", + "type-check": "tsc --noEmit", + "build": "node build.mjs" + }, + "author": "", + "license": "ISC", + "dependencies": { + "@aws-sdk/client-ec2": "^3.827.0", + "@aws-sdk/client-secrets-manager": "^3.825.0", + "date-fns": "^4.1.0" + }, + "devDependencies": { + "@types/node": "^20.10.5", + "esbuild": "^0.25.5", + "typescript": "^5.3.3" + } +} diff --git a/private-ca/server/src/generate-client-ssh-cert.ts b/private-ca/server/src/generate-client-ssh-cert.ts new file mode 100644 index 0000000..f052544 --- /dev/null +++ b/private-ca/server/src/generate-client-ssh-cert.ts @@ -0,0 +1,52 @@ +import fs from 'fs'; +import child_process from 'child_process'; +import util from 'util'; +import { format } from 'date-fns'; +import { CallerIdentityResponse, SecretData } from './types/index.js'; + +const exec = util.promisify(child_process.exec); +const clientCertValidityInDays = parseInt(process.env.clientCertValidityInDays ?? '1', 10); +const caKeyPath = "/tmp/client_ca"; +const publicKeyName = "ssh_client_rsa_key"; +const publicKeyPath = "/tmp/" + publicKeyName + ".pub"; +const certificatePath = "/tmp/" + publicKeyName + "-cert.pub"; + +export const generateClientSSHCert = async ( + callerIdentity: CallerIdentityResponse, + secret: SecretData, + certPubkey: string +): Promise => { + const arn = callerIdentity.GetCallerIdentityResponse.GetCallerIdentityResult.Arn; + const match = arn.match(/\/([^/]+)$/); + if (!match) { + throw new Error(`Invalid ARN format: ${arn}`); + } + const principal = match[1]; + const user_ca = Buffer.from(secret.user_ca, 'base64').toString('utf-8'); + + const decodedCertPubkey = Buffer.from(certPubkey, 'base64').toString('utf-8'); + fs.writeFileSync(caKeyPath, user_ca); + fs.writeFileSync(publicKeyPath, decodedCertPubkey); + + let result = await exec(`chmod 600 ${caKeyPath}`); + console.log('stdout:', result.stdout); + console.log('stderr:', result.stderr); + + const now = new Date(); + const validFrom = format(now, "yyyyMMddHHmmss"); + + const validUntil = new Date(now.getTime() + (clientCertValidityInDays * 24 * 60 * 60 * 1000)); + const validTo = format(validUntil, "yyyyMMddHHmmss"); + + const validityPeriod = `${validFrom}:${validTo}`; + + result = await exec( + `ssh-keygen -s ${caKeyPath} -t rsa-sha2-512 -I client_${principal} -n ${principal} -V ${validityPeriod} ${publicKeyPath}` + ); + + console.log('stdout:', result.stdout); + console.log('stderr:', result.stderr); + + const certificate = fs.readFileSync(certificatePath, 'utf8'); + return certificate; +}; \ No newline at end of file diff --git a/private-ca/server/src/generate-host-ssh-cert.ts b/private-ca/server/src/generate-host-ssh-cert.ts new file mode 100644 index 0000000..b70afb3 --- /dev/null +++ b/private-ca/server/src/generate-host-ssh-cert.ts @@ -0,0 +1,57 @@ +import fs from 'fs'; +import child_process from 'child_process'; +import util from 'util'; +import { getPublicIpAddress } from './get-public-ip-address.js'; +import { format } from 'date-fns'; +import { CallerIdentityResponse, SecretData } from './types/index.js'; + +const exec = util.promisify(child_process.exec); +const hostCertValidityInDays = parseInt(process.env.hostCertValidityInDays ?? '7', 10); + +export const generateHostSSHCert = async ( + callerIdentity: CallerIdentityResponse, + secret: SecretData, + certPubkey: string, + awsEC2Region: string +): Promise => { + const arn = callerIdentity.GetCallerIdentityResponse.GetCallerIdentityResult.Arn; + const match = arn.match(/\/([^/]+)$/); + if (!match) { + throw new Error(`Invalid ARN format: ${arn}`); + } + const instanceId = match[1]; + + const publicIp = await getPublicIpAddress(awsEC2Region, instanceId); + + const caKeyPath = "/tmp/host_ca"; + const publicKeyName = "ssh_host_rsa_key"; + const publicKeyPath = "/tmp/" + publicKeyName + ".pub"; + const certificatePath = "/tmp/" + publicKeyName + "-cert.pub"; + const host_ca = Buffer.from(secret.host_ca, 'base64').toString('utf-8'); + const decodedCertPubkey = Buffer.from(certPubkey, 'base64').toString('utf-8'); + + fs.writeFileSync(caKeyPath, host_ca); + fs.writeFileSync(publicKeyPath, decodedCertPubkey); + + let result = await exec(`chmod 600 ${caKeyPath}`); + console.log('stdout:', result.stdout); + console.log('stderr:', result.stderr); + + const now = new Date(); + const validFrom = format(now, "yyyyMMddHHmmss"); + + const validUntil = new Date(now.getTime() + (hostCertValidityInDays * 24 * 60 * 60 * 1000)); + const validTo = format(validUntil, "yyyyMMddHHmmss"); + + const validityPeriod = `${validFrom}:${validTo}`; + + result = await exec( + `ssh-keygen -s ${caKeyPath} -t rsa-sha2-512 -I host_${instanceId} -h -n ${publicIp} -V ${validityPeriod} ${publicKeyPath}` + ); + + console.log('stdout:', result.stdout); + console.log('stderr:', result.stderr); + + const certificate = fs.readFileSync(certificatePath, 'utf8'); + return certificate; +}; \ No newline at end of file diff --git a/private-ca/server/src/get-caller-identity.ts b/private-ca/server/src/get-caller-identity.ts new file mode 100644 index 0000000..87aded7 --- /dev/null +++ b/private-ca/server/src/get-caller-identity.ts @@ -0,0 +1,53 @@ +import https from 'https'; +import { LambdaEvent, CallerIdentityResponse } from './types/index.js'; + +export const getCallerIdentity = (event: LambdaEvent): Promise => { + const auth = event.auth; + const region = event.awsSTSRegion; + const host = 'sts.' + region + '.amazonaws.com'; + const path = '/'; + const payload = 'Action=GetCallerIdentity&Version=2011-06-15'; + + // Set the headers + const headers = { + 'accept': 'application/json', + 'Content-Type': 'application/x-www-form-urlencoded', + 'X-Amz-Date': auth.amzDate, + 'Authorization': auth.authorizationHeader, + 'X-Amz-Security-Token': auth.sessionToken, + 'Aud': 'FundwaveCA' + }; + + const options: https.RequestOptions = { + hostname: host, + path: path, + method: 'POST', + headers: headers + }; + + return new Promise((resolve, reject) => { + const req = https.request(options, (res) => { + let data = ''; + + res.on('data', (chunk: string) => { + data += chunk; + }); + + res.on('end', () => { + try { + const parsedData = JSON.parse(data) as CallerIdentityResponse; + resolve(parsedData); + } catch (err) { + reject(new Error(`Failed to parse response: ${err}`)); + } + }); + }); + + req.on('error', (error: Error) => { + reject(new Error(`Request failed: ${error.message}`)); + }); + + req.write(payload); + req.end(); + }); +}; \ No newline at end of file diff --git a/private-ca/server/src/get-public-ip-address.ts b/private-ca/server/src/get-public-ip-address.ts new file mode 100644 index 0000000..5c19f1b --- /dev/null +++ b/private-ca/server/src/get-public-ip-address.ts @@ -0,0 +1,26 @@ +import { EC2Client, DescribeInstancesCommand } from "@aws-sdk/client-ec2"; + +export const getPublicIpAddress = async ( + region: string, + instanceId: string +): Promise => { + const client = new EC2Client({ region }); + + const command = new DescribeInstancesCommand({ + InstanceIds: [instanceId], + }); + + const response = await client.send(command); + + const instance = response.Reservations?.[0]?.Instances?.[0]; + + if (!instance) { + throw new Error(`Instance ${instanceId} not found`); + } + + if (!instance.PublicIpAddress) { + throw new Error(`Instance ${instanceId} does not have a public IP address`); + } + + return instance.PublicIpAddress; +}; \ No newline at end of file diff --git a/private-ca/server/src/index_lambda.ts b/private-ca/server/src/index_lambda.ts new file mode 100644 index 0000000..c2d0e57 --- /dev/null +++ b/private-ca/server/src/index_lambda.ts @@ -0,0 +1,105 @@ +import { generateHostSSHCert } from './generate-host-ssh-cert.js'; +import { generateClientSSHCert } from './generate-client-ssh-cert.js'; +import { getCallerIdentity } from './get-caller-identity.js'; +import { getSecret } from './secret-manager-utils.js'; +import { + LambdaEvent, + LambdaResponse, +} from './types/index.js'; + +export const handler = async (event: any): Promise => { + try { + const parsedEvent: LambdaEvent = JSON.parse(event.body); + + // auth + const callerIdentity = await getCallerIdentity(parsedEvent); + const accountId = callerIdentity.GetCallerIdentityResponse.GetCallerIdentityResult.Account; + + // secret + const secret = await getSecret(accountId); + if (!secret) { + return { + statusCode: 401, + body: JSON.stringify({ + error: 'This AWS account is not configured to use this service' + }), + }; + } + + // action + switch (parsedEvent.action) { + case "generateHostSSHCert": { + if (!parsedEvent.certPubkey) { + return { + statusCode: 400, + body: JSON.stringify({ error: 'Missing certPubkey' }), + }; + } + if (!parsedEvent.awsEC2Region) { + return { + statusCode: 400, + body: JSON.stringify({ error: 'Missing awsEC2Region' }), + }; + } + + const hostSSHCert = await generateHostSSHCert( + callerIdentity, + secret, + parsedEvent.certPubkey, + parsedEvent.awsEC2Region + ); + + return { + statusCode: 200, + body: JSON.stringify({ + certificate: Buffer.from(hostSSHCert).toString('base64'), + 'user_ca.pub': secret['user_ca.pub'] + }) + }; + } + + case "generateClientSSHCert": { + if (!parsedEvent.certPubkey) { + return { + statusCode: 400, + body: JSON.stringify({ error: 'Missing certPubkey' }), + }; + } + const clientSSHCert = await generateClientSSHCert( + callerIdentity, + secret, + parsedEvent.certPubkey + ); + + return { + statusCode: 200, + body: JSON.stringify({ + certificate: Buffer.from(clientSSHCert).toString('base64'), + 'host_ca.pub': secret['host_ca.pub'] + }) + }; + } + + case "getHostCAPublicKey": { + return { + statusCode: 200, + body: JSON.stringify({ 'host_ca.pub': secret['host_ca.pub'] }) + }; + } + + default: + console.log(`Invalid Action: ${parsedEvent.action}`, { event: parsedEvent }); + return { + statusCode: 400, + body: JSON.stringify({ error: 'Invalid Action' }), + }; + } + } catch (err) { + console.error('Error in cert signing handler:', err); + const errorMessage = err instanceof Error ? err.message : 'Internal server error'; + return { + statusCode: 500, + body: JSON.stringify({ error: errorMessage }), + }; + } +}; \ No newline at end of file diff --git a/private-ca/server/src/secret-manager-utils.ts b/private-ca/server/src/secret-manager-utils.ts new file mode 100644 index 0000000..0345809 --- /dev/null +++ b/private-ca/server/src/secret-manager-utils.ts @@ -0,0 +1,24 @@ +import { SecretsManagerClient, GetSecretValueCommand } from "@aws-sdk/client-secrets-manager"; +import { SecretData } from './types/index.js'; + +export const getSecret = async ( + accountId: string +): Promise => { + const client = new SecretsManagerClient(); + try { + const command = new GetSecretValueCommand({ + SecretId: `privateCA_${accountId}_secret` + }); + const response = await client.send(command); + + if (!response.SecretString) { + return null; + } + + const secret = JSON.parse(response.SecretString) as SecretData; + return secret; + } catch (err) { + console.error(`Failed to retrieve secret for account ${accountId} from AWS Secrets Manager:`, err); + return null; + } +}; \ No newline at end of file diff --git a/private-ca/server/src/types/index.ts b/private-ca/server/src/types/index.ts new file mode 100644 index 0000000..9e9c48e --- /dev/null +++ b/private-ca/server/src/types/index.ts @@ -0,0 +1,36 @@ +export interface LambdaEvent { + body: string; + certPubkey: string; + action: 'generateHostSSHCert' | 'generateClientSSHCert' | 'getHostCAPublicKey'; + awsEC2Region?: string; + awsSTSRegion: string; + auth: AuthCredentials; +} + +export interface AuthCredentials { + amzDate: string; + authorizationHeader: string; + sessionToken: string; +} + +export interface CallerIdentityResponse { + GetCallerIdentityResponse: { + GetCallerIdentityResult: { + Account: string; + Arn: string; + UserId: string; + }; + }; +} + +export interface SecretData { + host_ca: string; + 'host_ca.pub': string; + user_ca: string; + 'user_ca.pub': string; +} + +export interface LambdaResponse { + statusCode: number; + body: string; +} \ No newline at end of file diff --git a/private-ca/server/tsconfig.json b/private-ca/server/tsconfig.json new file mode 100644 index 0000000..3ce3daf --- /dev/null +++ b/private-ca/server/tsconfig.json @@ -0,0 +1,23 @@ +{ + "compilerOptions": { + "target": "ES2020", + "module": "ESNext", + "moduleResolution": "node", + "lib": ["ES2020"], + "outDir": "./dist", + "rootDir": "./src", + "strict": true, + "esModuleInterop": true, + "allowSyntheticDefaultImports": true, + "skipLibCheck": true, + "forceConsistentCasingInFileNames": true, + "resolveJsonModule": true, + "removeComments": true, + "noImplicitAny": true, + "noImplicitReturns": true, + "noFallthroughCasesInSwitch": true, + "exactOptionalPropertyTypes": true + }, + "include": ["src/**/*"], + "exclude": ["node_modules", "dist", "**/*.test.ts"] +} \ No newline at end of file diff --git a/private-ca/update-server-on-lambda.sh b/private-ca/update-server-on-lambda.sh new file mode 100755 index 0000000..d5f512a --- /dev/null +++ b/private-ca/update-server-on-lambda.sh @@ -0,0 +1,20 @@ +#!/bin/bash + +FUNCTION_NAME=${1:-'privateCA'} +REGION=${2:-'eu-central-1'} +PROFILE=${3:-''} + +[[ -n "$PROFILE" ]] && PROFILE="--profile $PROFILE" || PROFILE="" + +(cd server +npm i +npm run build +cd dist && zip -qr ../../lambda.zip .) + +aws lambda update-function-code \ + --function-name "$FUNCTION_NAME" \ + --zip-file fileb://lambda.zip \ + --region "$REGION" \ + $PROFILE + +rm -r lambda.zip diff --git a/ssm-env-util/package-lock.json b/ssm-env-util/package-lock.json index 6bd0674..fb0020f 100644 --- a/ssm-env-util/package-lock.json +++ b/ssm-env-util/package-lock.json @@ -1,12 +1,12 @@ { "name": "ssm-env-util", - "version": "1.0.0", + "version": "1.0.1-update/privateCA.0", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "ssm-env-util", - "version": "1.0.0", + "version": "1.0.1-update/privateCA.0", "license": "ISC", "dependencies": { "@aws-sdk/client-ssm": "^3.624.0", diff --git a/ssm-env-util/package.json b/ssm-env-util/package.json index c5336aa..0caab26 100644 --- a/ssm-env-util/package.json +++ b/ssm-env-util/package.json @@ -1,6 +1,6 @@ { "name": "ssm-env-util", - "version": "1.0.0", + "version": "1.0.1-update/privateCA.0", "description": "get parameters from ssm by parsing .env.template", "type": "module", "main": "index.js",