AWS Lambda with the AWS CLI

For AWS users who want the smallest possible footprint: three resources (an IAM role, a function, a Function URL), created with plain aws commands. There is no state file, no S3 staging bucket (zips under 50 MB upload directly), and nothing to host while idle.

The binary handles Lambda Function URL events natively — no HTTP server, no container image, no adapter layer. Deployed as bootstrap on provided.al2023, it detects the Lambda runtime and serves events directly; the request and response format is identical to the standalone server, so the GitHub Actions workflow is unchanged.

If you prefer declarative infrastructure, the same setup is available as Terraform.

1. Build the zip

The zip contains the binary and the policy. The policy is loaded once at cold start (OIDC_SSH_CA_CONFIG overrides the path; the default is policy.yaml in the zip).

GOOS=linux GOARCH=arm64 CGO_ENABLED=0 \
  go build -trimpath -ldflags="-s -w" -tags lambda.norpc \
  -o bootstrap ./cmd/oidc-ssh-ca
zip lambda.zip bootstrap policy.yaml

2. Create the execution role

CloudWatch Logs is the only permission the function needs:

aws iam create-role --role-name oidc-ssh-ca-lambda \
  --assume-role-policy-document '{
    "Version": "2012-10-17",
    "Statement": [{
      "Effect": "Allow",
      "Principal": {"Service": "lambda.amazonaws.com"},
      "Action": "sts:AssumeRole"
    }]
  }'

aws iam attach-role-policy --role-name oidc-ssh-ca-lambda \
  --policy-arn arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole

3. Create the function

The CA key goes into the OIDC_SSH_CA_KEY environment variable (Lambda environment variables are encrypted at rest). The key is multi-line, so build the --environment JSON with jq instead of inlining it. If the role was created seconds ago, IAM propagation may make this fail once — retry.

aws lambda create-function \
  --function-name oidc-ssh-ca \
  --runtime provided.al2023 \
  --architectures arm64 \
  --handler bootstrap \
  --role "$(aws iam get-role --role-name oidc-ssh-ca-lambda --query Role.Arn --output text)" \
  --zip-file fileb://lambda.zip \
  --timeout 10 \
  --memory-size 128 \
  --environment "$(jq -n --rawfile key ca_key '{Variables: {OIDC_SSH_CA_KEY: $key}}')"

# Cap the function at a single instance.
aws lambda put-function-concurrency \
  --function-name oidc-ssh-ca \
  --reserved-concurrent-executions 1

4. Create the Function URL

Public invocation is intentional: /sign authenticates callers by verifying their OIDC token, the same model as running the server on a public host.

aws lambda create-function-url-config \
  --function-name oidc-ssh-ca \
  --auth-type NONE

aws lambda add-permission \
  --function-name oidc-ssh-ca \
  --statement-id allow-public-function-url \
  --action lambda:InvokeFunctionUrl \
  --principal "*" \
  --function-url-auth-type NONE

The first command prints the FunctionUrl — that is the OIDC_SSH_CA_URL for the GitHub Actions workflow.

Operations

Update the binary or the policy — rebuild the zip and redeploy (there is no SIGHUP reload in Lambda; the zip is the unit of change):

zip lambda.zip bootstrap policy.yaml
aws lambda update-function-code --function-name oidc-ssh-ca \
  --zip-file fileb://lambda.zip

Emergency stop — immediate, no redeploy:

aws lambda put-function-concurrency \
  --function-name oidc-ssh-ca \
  --reserved-concurrent-executions 0

Audit logs go to CloudWatch Logs (/aws/lambda/oidc-ssh-ca). Set a CloudWatch alarm on invocation count to notice unexpected traffic against the public URL.

Tear down:

aws lambda delete-function --function-name oidc-ssh-ca
aws iam detach-role-policy --role-name oidc-ssh-ca-lambda \
  --policy-arn arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole
aws iam delete-role --role-name oidc-ssh-ca-lambda