Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
31 changes: 31 additions & 0 deletions .github/workflows/update-ssh-verification-lambda.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
name: Update SSH Verification Lambda on file change

on:
push:
paths:
- trusted-fingerprint/lambda/server.py
- .github/workflows/update-ssh-verification-lambda.yml

permissions:
id-token: write
contents: read

jobs:
update-lambda:
runs-on: "ubuntu-22.04"

steps:
- name: Checkout code
uses: actions/checkout@v3

- name: Configure AWS Credentials
uses: aws-actions/configure-aws-credentials@v2
with:
aws-region: ${{ secrets.AWS_REGION }}
role-to-assume: ${{ secrets.AWS_SSH_UPDATE_ROLE_ARN }}
role-session-name: UpdateSSHLambda

- name: Run update-lambda.sh
run: |
cd trusted-fingerprint/lambda/
bash update-lambda.sh
1 change: 1 addition & 0 deletions .talismanrc
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
threshold: medium
fileignoreconfig:
- filename: aws-credentials-utils/get-credentials-mac.sh
checksum: fb30c4bc225fc25b96463246d419bb98efaf9969b7965f81e009d42bbcc7eff5
Expand Down
71 changes: 71 additions & 0 deletions trusted-fingerprint/client/verify-ssh-key.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
#!/bin/bash

HOST=$1
USER_KEY=$2
VERIFY_SSH_LAMBDA_URL=${3:-$VERIFY_SSH_LAMBDA_URL}
VERIFY_SSH_LAMBDA_TOKEN=${4:-$VERIFY_SSH_LAMBDA_TOKEN}
KNOWN_HOSTS=${5:-"/home/$USER/.ssh/known_hosts"}

USER_KEY_TYPE=$(echo $USER_KEY | cut -d " " -f 1)

if [[ $USER_KEY_TYPE == "ssh-ed25519" ]]; then
host_key=$(ssh-keyscan -t ed25519 $HOST | awk '{print $3}')
elif [[ $USER_KEY_TYPE == "ssh-rsa" ]]; then
host_key=$(ssh-keyscan -t rsa $HOST | awk '{print $3}')
elif [[ $USER_KEY_TYPE == "ecdsa-sha2-nistp256" ]]; then
host_key=$(ssh-keyscan -t ecdsa $HOST | awk '{print $3}')
fi

# Check if the key is empty
if [[ -z "$host_key" ]]; then
echo "The corresponding $USER_KEY_TYPE key could not be found on the host"
exit 1
fi

hashed_hostname=$(echo -n "$HOST" | sha256sum | cut -d " " -f 1 | awk '{ print $1 }')

# Check if the hostname (hashed or unhashed) exists in known hosts
if [[ $(grep -q "$HOST $USER_KEY_TYPE" "$KNOWN_HOSTS"; echo $?) -eq 0 ||
$(grep -q "$hashed_hostname $USER_KEY_TYPE" "$KNOWN_HOSTS"; echo $?) -eq 0
]]; then
echo "Key found in known_hosts."
exit 0
else
echo "Key not found in known_hosts."
echo "Attempting key verification..."

lambda_response_status=$(curl -sw '%{http_code}' \
-o key.txt \
-X 'POST' \
-H 'Content-Type: application/json' \
-d '{
"Host": '"\"${HOST}\""',
"KeyType": '"\"${USER_KEY_TYPE}\""',
"Authorization": '"\"Bearer ${VERIFY_SSH_LAMBDA_TOKEN}\""'
}' \
$VERIFY_SSH_LAMBDA_URL)

if [[ $lambda_response_status != "200" ]]; then
echo "Encountered server error"
exit 1
fi

lambda_response_key=$(cat key.txt)
rm key.txt

if [[
$host_key == $lambda_response_key
]]; then

echo "Key verified."
echo "Adding keys to known hosts"
echo "$HOST $USER_KEY_TYPE $lambda_response_key" >> $KNOWN_HOSTS
exit 0

else
echo "Key could not be verified"
echo "Host Key ($host_key) does not match with lambda response ($lambda_response_key)"
exit 1
fi

fi
93 changes: 93 additions & 0 deletions trusted-fingerprint/lambda/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
# SSH Key Retrieval

This function, deployed as an AWS lambda function retrieves the SSH public key from a remote server using the Paramiko library. The function expects three parameters in the event payload: `Host`, `KeyType` and `Authorization`. It establishes an SSH connection to the specified host using the provided URL and retrieves the public key using the specified key type.

## Dependencies

The function requires the Paramiko library to be installed. This is included as a layer when creating the Lambda function.

## Usage

### To set up as a Lambda function, follow these steps:

1. Create a new IAM role with `AWSLambdaBasicExecutionRole` policy.
2. Copy the Role ARN and set it as `VERIFY_SSH_LAMBDA_ROLE_ARN` environment variable (Alternatively, you can pass it as an input parameter to `deploy-lambda-with-layer.sh`).
3. Run `deploy-lambda-with-layer.sh` to deploy the lambda function along with the layer.
4. To update the lambda function, edit `server.py` then run `update-lambda.sh`.
5. To update the layer, modify `requirements.txt` and then run `update-layer.sh`.
6. Add an environment variable called `SECRET_TOKEN` and set its value to a token to be used in Authorization header while invoking the lambda.
6. Configure an API via API Gateway as an event source to trigger the Lambda function with the required event payload.

### To set up an API using API Gateway, follow these steps:

1. Click on `Add trigger` in lambda function homepage.
2. Select `API Gateway` as source.
3. Select `Create a new API` and choose `HTTP API`.
4. Select `Open` under security and click on `Add`.

### Event Payload

The Lambda function expects the following parameters in the `event` object:

- `Host` (string): The URL or IP address of the remote server to connect to.
- `KeyType` (string): The type of SSH key to retrieve (e.g., "ssh-rsa", "ssh-ed25519", etc.).
- `Authorization` (string): The authorization token with the form `Bearer <token>`.

### Usage Sample

```bash
curl -X 'POST' \
-H 'Content-Type: application/json' \
-d '{
"Host": "<my-server-hostname>",
"KeyType": "ssh-rsa",
"Authorization": "Bearer <token>"
}' \
<lambda_url>
```

### Return Value

The Lambda function returns a JSON object with the following properties:

- `statusCode` (integer): The HTTP status code of the response.
- `body` (string): The base64-encoded string representation of the retrieved SSH public key (in case there are no errors).


## Actions workflow

To set up Github Actions Workflow, follow these steps:

1. Create a new role in IAM dashboard.
2. Select `Web Identity` under `Trusted entity type`.
3. Click on `Create new` under `Identity provider`.
4. Select `OpenID Connect` as `Provider type`.
5. For the `Provider URL`: Use `https://token.actions.githubusercontent.com`
6. For the `Audience`: Use `sts.amazonaws.com`.
7. Create a role with this identity provider.
8. Add the following policy to the role under permission policy:
```json
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "VisualEditor0",
"Effect": "Allow",
"Action": "lambda:UpdateFunctionCode",
"Resource": <ARN of the lambda function>
}
]
}
```
9. Edit `Trusted entities` under `Trust Relationships` to add the `sub` field to the validation conditions. For example:
```json
"Condition": {
"StringEquals": {
"token.actions.githubusercontent.com:aud": "sts.amazonaws.com",
"token.actions.githubusercontent.com:sub": "repo:<organization>/<reponame>:ref:refs/heads/master"
}
}
```
10. Add the following workflow secrets:
- `AWS_REGION` - The AWS region in which the lambda is created
- `AWS_SSH_UPDATE_ROLE_ARN` - The ARN for the role created above
48 changes: 48 additions & 0 deletions trusted-fingerprint/lambda/deploy-lambda-with-layer.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
#!/bin/bash

# Variables
FUNCTION_NAME=${1:-"trusted-fingerprint"}
LAYER_NAME=${2:-"paramiko-layer"}
VERIFY_SSH_LAMBDA_ROLE_ARN=${3:-$VERIFY_SSH_LAMBDA_ROLE_ARN}
AWS_REGION=${4:-"ap-southeast-1"}
AWS_PROFILE=${5:-"default"}

LAMBDA_FUNCTION_ZIP_FILE="lambda-function.zip"
LAYER_ZIP_FILE="layer.zip"

# Create a virtual environment and install dependencies
# As Paramiko has system level dependencies, we must package them in the same environment that lambda uses.
# Here we use the AWS Serverless Application Model (SAM) image to emulate the python3.8 AWS Lambda runtime.
# https://gallery.ecr.aws/sam/build-python3.8
docker run -v "$PWD":/var/task "public.ecr.aws/sam/build-python3.8" /bin/sh -c "python3.8 -m venv env && source env/bin/activate && pip install --platform manylinux2010_x86_64 --implementation cp --python 3.8 --only-binary=:all: --upgrade -r requirements.txt -t python/lib/python3.8/site-packages; exit"

# Package the Lambda function and the Paramiko layer
zip -r9 $LAMBDA_FUNCTION_ZIP_FILE server.py
zip -r9 $LAYER_ZIP_FILE python

# Create the Lambda layer
LAYER_ARN=$(aws lambda publish-layer-version \
--layer-name $LAYER_NAME \
--description "Layer for Paramiko" \
--compatible-runtimes python3.8 \
--zip-file fileb://$LAYER_ZIP_FILE \
--query 'LayerVersionArn' \
--region "${AWS_REGION}" \
--profile "${AWS_PROFILE}" \
--output text)

# Create the Lambda function
aws lambda create-function \
--function-name $FUNCTION_NAME \
--runtime python3.8 \
--handler server.lambda_handler \
--zip-file fileb://$LAMBDA_FUNCTION_ZIP_FILE \
--layers $LAYER_ARN \
--role $VERIFY_SSH_LAMBDA_ROLE_ARN \
--region "${AWS_REGION}" \
--profile "${AWS_PROFILE}"

# Clean up
sudo rm -r env python
rm lambda-function.zip layer.zip
docker rmi -f public.ecr.aws/sam/build-python3.8
1 change: 1 addition & 0 deletions trusted-fingerprint/lambda/requirements.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Paramiko
44 changes: 44 additions & 0 deletions trusted-fingerprint/lambda/server.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
import paramiko
import json
import os

def lambda_handler(event, context):
try:
event_body = json.loads(event['body'])

if 'Authorization' not in event_body:
return {
'statusCode': 403,
'body': 'Forbidden'
}

auth = event_body['Authorization'].split()
host = event_body['Host']
key_type = event_body['KeyType']

secret_token = os.environ['SECRET_TOKEN']

if len(auth) != 2 or auth[0] != "Bearer" or auth[1] != secret_token:
return {
'statusCode': 403,
'body': 'Forbidden'
}

transport = paramiko.Transport(host)
transport.get_security_options().key_types = [key_type]
transport.connect()

key = transport.get_remote_server_key()
key_body = key.get_base64()

transport.close()

return {
'statusCode': 200,
'body': key_body
}
except:
return {
'statusCode': 500,
'body': 'Internal Server Error '
}
20 changes: 20 additions & 0 deletions trusted-fingerprint/lambda/update-lambda.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
#!/bin/bash

# Variables
FUNCTION_NAME=${1:-"VerifySSHKey"}
AWS_REGION=${2:-"ap-south-1"}
AWS_PROFILE=${3:-"default"}
ZIP_FILE="lambda-function.zip"

# Package the updated Lambda function code
zip -r9 $ZIP_FILE server.py

# Update the Lambda function code
aws lambda update-function-code \
--function-name $FUNCTION_NAME \
--zip-file fileb://$ZIP_FILE \
--region "${AWS_REGION}" \
--profile "${AWS_PROFILE}"

# Clean up
rm $ZIP_FILE
39 changes: 39 additions & 0 deletions trusted-fingerprint/lambda/update-layer.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
#!/bin/bash

# Variables
LAYER_NAME=${1:-"paramiko-layer"}
FUNCTION_NAME=${2:-"VerifySSHKey"}
AWS_REGION=${3:-"ap-south-1"}
AWS_PROFILE=${4:-"default"}
ZIP_FILE="layer.zip"

# Create a virtual environment and install dependencies
python3.8 -m venv env
source env/bin/activate

docker run -v "$PWD":/var/task "public.ecr.aws/sam/build-python3.8" /bin/sh -c "pip install --platform manylinux2010_x86_64 --implementation cp --python 3.8 --only-binary=:all: --upgrade -r requirements.txt -t python/lib/python3.8/site-packages; exit"

# Package the updated layer code
zip -r9 $ZIP_FILE python

# Publish a new version of the Lambda layer
LAYER_ARN=$(aws lambda publish-layer-version \
--layer-name $LAYER_NAME \
--description "My Layer" \
--compatible-runtimes python3.8 \
--zip-file fileb://$ZIP_FILE \
--query 'LayerVersionArn' \
--output text\
--region "${AWS_REGION}" \
--profile "${AWS_PROFILE}")

# Update the Lambda function(s) that use the layer
aws lambda update-function-configuration \
--function-name $FUNCTION_NAME \
--layers $LAYER_ARN \
--region "${AWS_REGION}" \
--profile "${AWS_PROFILE}"

# Clean up
rm -r env python
rm $ZIP_FILE