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.
There is no Lambda-specific code in the binary: on Lambda it runs the same
ordinary HTTP server as everywhere else, behind the AWS-provided
Lambda Web Adapter (LWA)
layer, which converts each Function URL event into a POST /sign request. No
container image is needed. 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, the run.sh startup script (the LWA handler), and
the policy. The policy is loaded once at cold start from policy.yaml in the
zip. run.sh is in the repository at examples/lambda/run.sh.
The linux/arm64 binary comes from the prebuilt
ghcr.io/atsuoishimoto/oidc-ssh-ca
image — no Go toolchain or cross-compilation needed (pin a release tag instead
of latest in production). docker cp pulls the binary straight out of the
image; the container is never started:
docker create --platform linux/arm64 --name oidc-ssh-ca-extract \
ghcr.io/atsuoishimoto/oidc-ssh-ca:latest
docker cp oidc-ssh-ca-extract:/oidc-ssh-ca ./oidc-ssh-ca
docker rm oidc-ssh-ca-extract
cp examples/lambda/run.sh .
zip lambda.zip oidc-ssh-ca run.sh policy.yaml
run.sh must keep its executable bit inside the zip (creating the zip on
Windows is a known way to lose it).
If you have a Go toolchain and prefer to build from source instead, see Building for the cross-compile command.
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 function uses the LWA layer: its handler is run.sh, and
AWS_LAMBDA_EXEC_WRAPPER=/opt/bootstrap tells the runtime to start through the
adapter. Pick the layer ARN for your architecture and region from the
LWA releases
(LambdaAdapterLayerArm64 for arm64); the version below may be newer by the
time you deploy.
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.
LWA_LAYER="arn:aws:lambda:${AWS_REGION}:753240598075:layer:LambdaAdapterLayerArm64:28"
aws lambda create-function \
--function-name oidc-ssh-ca \
--runtime provided.al2023 \
--architectures arm64 \
--handler run.sh \
--layers "$LWA_LAYER" \
--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, AWS_LAMBDA_EXEC_WRAPPER: "/opt/bootstrap"}}')"
# 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 oidc-ssh-ca run.sh 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