Skip to content
Open
Show file tree
Hide file tree
Changes from 16 commits
Commits
Show all changes
37 commits
Select commit Hold shift + click to select a range
798e263
Add files for private CA
sudo-rgorai Jun 22, 2023
84a9e1d
Take AWS region as input
sudo-rgorai Jun 23, 2023
1e8aa25
Remove action for generating root X.509 certificate
sudo-rgorai Jun 24, 2023
123b832
Use current timestamp for auth header
sudo-rgorai Jun 24, 2023
ce72f23
Accept lambda event as params
sudo-rgorai Jun 24, 2023
571e245
Use region for all aws services
sudo-rgorai Jun 26, 2023
abcbce9
Use exact pattern matching for deleting temporary files
sudo-rgorai Jun 26, 2023
af7756d
Fix filenames in deploy-resources.sh
sudo-rgorai Jun 26, 2023
2e1b9b7
Change CA action from get to generate
sudo-rgorai Jun 26, 2023
8039e58
Delete secret.json after use
sudo-rgorai Jun 26, 2023
ba4deb7
Delete package-lock.json
sudo-rgorai Jun 26, 2023
e27dff0
Fix ssh client cert validity
sudo-rgorai Jun 26, 2023
c30758f
Use pubkey file instead of string for generating certificate
sudo-rgorai Jun 26, 2023
99afb54
Do not use response.json
sudo-rgorai Jun 26, 2023
eb67f93
Base64 encode response certificate
sudo-rgorai Jul 4, 2023
99a8b9b
Invoke function using lambda URL instead of AWS CLI
sudo-rgorai Jul 4, 2023
b9c2c48
Add -h help tag and handle invalid action
sudo-rgorai Jul 6, 2023
d38a12c
Certificate validity should be decided by CA not by the subject
sudo-rgorai Jul 6, 2023
1b7802e
Check for existing certificates and their expiration
sudo-rgorai Jul 6, 2023
4c70c44
Add certificate expiration buffer of 5 minutes
sudo-rgorai Jul 7, 2023
6efcdb3
Use awscurl instead of curl to generate certs
sudo-rgorai Jul 11, 2023
ff72a42
Do not use awscurl
sudo-rgorai Jul 11, 2023
c854587
reorganize directories
Jul 11, 2023
d67a780
add awsprofile, let server decide it's own secret region
Jul 11, 2023
e073fbc
Add audience header
sudo-rgorai Jul 12, 2023
b4f21c5
Use docker container
sudo-rgorai Jul 12, 2023
b078b61
Fix server deployment issues
sudo-rgorai Jul 13, 2023
6424640
Add instruction for AWS secrets region environment variable
sudo-rgorai Jul 14, 2023
a128e39
Specify certificate type
sudo-rgorai Jul 14, 2023
d4fdede
private-ca: use variables for validity, cert details
Aug 4, 2023
28b4de3
merge main
Aug 4, 2023
2720292
Added cron job to Docker Container to regenerate certs everyday
Sep 25, 2023
23f4391
added override default cert location for curl
Sep 25, 2023
312cda7
workflow to push private ca container to docker hub
Oct 6, 2023
9df7571
updated push path for privateCA workflow
Oct 13, 2023
dfa428c
fix: set curl cert bundle path
Oct 26, 2023
315bc95
fix: changed cd path [skip ci]
Oct 26, 2023
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
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
private-ca/lambda/node_modules/
private-ca/lambda/package-lock.json
.talismanrc
40 changes: 40 additions & 0 deletions private-ca/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
# Private Certificate Authority (CA) for SSH and SSL Certificates

This project provides a private Certificate Authority (CA) implementation for generating both SSH and SSL certificates. It allows you to issue certificates for SSH hosts and users, as well as client SSL certificates for secure communication.

## Installation


Deploy the resources by running:

```bash
./deploy-resources.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/update access to created secret
- An openSSH layer to facilitate SSH operations
- The lambda function to act as a privateCA


## Usage

Certificates can be generated by running:

```bash
./generate-certificate.sh
```

You can pass the following params to modify the payload:

- `CA_ACTION`
- generateHostSSHCert
- generateClientSSHCert
- generateClientX509Cert
- `CERT_PUBKEY` - Public Key of the client to be signed for generating the certificate
- `CERT_VALIDITY` - Validity of the generated certificate in days
- `AWS_STS_REGION` - AWS region for generating sts token
- `AWS_SECRETS_REGION` - AWS region of the secret
- `CA_LAMBDA_FUNCTION_NAME` - Name of the CA lambda function
30 changes: 30 additions & 0 deletions private-ca/aws-auth-header.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import sys
from datetime import datetime
import boto3
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().strftime('%Y%m%dT%H%M%SZ')
}
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)
112 changes: 112 additions & 0 deletions private-ca/deploy-resources.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
#!/bin/bash

SECRET_NAME=${1:-"privateCA"}
ROLE_NAME=${2:-"privateCALambdaRole"}
POLICY_NAME=${3:-"PrivateCAPolicy"}
LAYER_NAME=${4:-"openssh"}
FUNCTION_NAME=${5:-"privateCA"}
AWS_REGION=${6:-"ap-south-1"}

################## Secret ##################

# Generate Keys
ssh-keygen -t rsa -b 4096 -f host_ca -C host_ca -N ""
ssh-keygen -t rsa -b 4096 -f user_ca -C user_ca -N ""

openssl genrsa -out key.pem 2048
openssl rsa -in key.pem -outform PEM -pubout -out public.pem
openssl req -new -x509 -key key.pem -out root.crt -days 365 -subj "/C=US/ST=California/L=YourCity/O=Fundwave/OU=Fundwave/CN=FundwaveCA"

HOST_CA_PRIVATE_KEY=$(cat host_ca | base64 -w 0)
HOST_CA_PUBLIC_KEY=$(cat host_ca.pub | base64 -w 0)
USER_CA_PRIVATE_KEY=$(cat user_ca | base64 -w 0)
USER_CA_PUBLIC_KEY=$(cat user_ca.pub | base64 -w 0)
ROOT_SSL_PRIVATE_KEY=$(cat key.pem | base64 -w 0)
ROOT_SSL_PUBLIC_KEY=$(cat public.pem | base64 -w 0)
ROOT_SSL_CERT=$(cat root.crt | base64 -w 0)

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}\",\"root_ssl_private_key\": \"${ROOT_SSL_PRIVATE_KEY}\",\"root_ssl_public_key\": \"${ROOT_SSL_PUBLIC_KEY}\", \"rootX509cert\": \"${ROOT_SSL_CERT}\"}" | jq . > secret.json

# Create Secret
SECRET_ARN=$(aws secretsmanager create-secret \
--name $SECRET_NAME \
--secret-string file://secret.json \
--region $AWS_REGION | jq ".ARN" | tr -d '"')

# Clean up
rm host_ca host_ca.pub user_ca user_ca.pub key.pem public.pem root.crt 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 \
--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\",\"secretsmanager:UpdateSecret\"],\"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 --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

# 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 -yr ./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 \
--query 'LayerVersionArn' \
--output text)
cd ..

# Clean up
sudo rm -r openssh-layer/
############################################

################## Lambda ##################

# Create lambda function
cd lambda
npm i
zip -r ./lambda.zip .
mv lambda.zip ../
cd ..

aws lambda create-function \
--function-name $FUNCTION_NAME \
--runtime nodejs18.x \
--region $AWS_REGION \
--handler index.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

FUNCTION_URL=$(aws lambda create-function-url-config --function-name "privateCA" --auth-type "NONE" | jq -r ".FunctionUrl")

echo "CA deployed at URL:"
echo "${FUNCTION_URL}"

# Clean up
sudo rm -r lambda/node_modules/ lambda/package-lock.json lambda.zip
###########################################
34 changes: 34 additions & 0 deletions private-ca/generate-certificate.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
#!/bin/bash

CA_ACTION=${1}
CERT_PUBKEY_FILE=${2}
CA_LAMBDA_URL=${3}
CERT_VALIDITY=${4:-"1"}
AWS_STS_REGION=${5:-"ap-south-1"}
AWS_SECRETS_REGION=${6:-"ap-south-1"}
CA_LAMBDA_FUNCTION_NAME=${7:-"privateCA"}

CERT_PUBKEY=$(cat ${CERT_PUBKEY_FILE} | base64 -w 0)

# Temporary Credentials
TEMP_CREDS=$(aws sts get-session-token)

ACCESS_KEY_ID=$(echo $TEMP_CREDS | jq -r ".Credentials.AccessKeyId")
SECRET_ACCESS_KEY=$(echo $TEMP_CREDS | jq -r ".Credentials.SecretAccessKey")
SESSION_TOKEN=$(echo $TEMP_CREDS | jq -r ".Credentials.SessionToken")

# Auth Headers
python -m venv env && source env/bin/activate
pip install boto3

output=$(python aws-auth-header.py $ACCESS_KEY_ID $SECRET_ACCESS_KEY $SESSION_TOKEN $AWS_STS_REGION)
auth_header=$(echo $output | jq -r ".Authorization")
date=$(echo $output | jq -r ".Date")

EVENT_JSON=$(echo "{\"auth\":{\"amzDate\":\"${date}\",\"authorizationHeader\":\"${auth_header}\",\"sessionToken\":\"${SESSION_TOKEN}\"},\"certValidity\":\"${CERT_VALIDITY}\",\"certPubkey\":\"${CERT_PUBKEY}\",\"action\":\"${CA_ACTION}\",\"awsSTSRegion\":\"${AWS_STS_REGION}\",\"awsSecretsRegion\":\"${AWS_SECRETS_REGION}\"}")

curl "${CA_LAMBDA_URL}" -H 'content-type: application/json' -d "$EVENT_JSON" | tr -d '"' | base64 -d > certificate

# Clean up
deactivate
sudo rm -r env/
31 changes: 31 additions & 0 deletions private-ca/lambda/generate-client-ssh-cert.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import fs from 'fs';
import child_process from 'child_process';
import util from 'util';

const exec = util.promisify(child_process.exec);

export const signClientSSHCertificate = async (callerIdentity, secret, certValidity, certPubkey) => {

const arn = callerIdentity.GetCallerIdentityResponse.GetCallerIdentityResult.Arn;
const roleName = arn.match(/\/([^/]+)$/)?.[1];

const caKeyPath = "/tmp/client_ca";
const publicKeyName = "ssh_client_rsa_key";
const publicKeyPath = "/tmp/" + publicKeyName + ".pub";
const certificatePath = "/tmp/" + publicKeyName + "-cert.pub";
const user_ca = Buffer.from(secret.user_ca, 'base64').toString('utf-8');
certPubkey = Buffer.from(certPubkey, 'base64').toString('utf-8');
fs.writeFileSync(caKeyPath, user_ca);
fs.writeFileSync(publicKeyPath, certPubkey);

let { stdout, stderr } = await exec(`chmod 600 ${caKeyPath}`);
console.log('stdout:', stdout);
console.log('stderr:', stderr);

({ stdout, stderr } = await exec(`ssh-keygen -s ${caKeyPath} -I client_${roleName} -n ${roleName} -V +${certValidity}d ${publicKeyPath}`));
console.log('stdout:', stdout);
console.log('stderr:', stderr);

const certificate = fs.readFileSync(certificatePath, 'utf8');
return certificate;
};
68 changes: 68 additions & 0 deletions private-ca/lambda/generate-client-x509-cert.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
import forge from 'node-forge';
import md from 'node-forge';

export const generateClientX509Cert = async (callerIdentity, secret, event) => {

const pki = forge.pki;

const arn = callerIdentity.GetCallerIdentityResponse.GetCallerIdentityResult.Arn;
const roleName = arn.match(/\/([^/]+)$/)?.[1];

// Load the root certificate private key from a file or string
const rootKeyPem = Buffer.from(secret.root_ssl_private_key, 'base64').toString('utf-8');
const rootKey = pki.privateKeyFromPem(rootKeyPem);

// Load the root certificate public key from a file or string
let rootCertKey = 'rootX509cert';
if(!(rootCertKey in secret))
{
console.log("No root certificate found. Aborting creation of client X.509 certificate.");
return {
statusCode: 500,
body: "No root certificate found."
};
}
let rootCertPem = Buffer.from(secret['rootX509cert'], 'base64').toString('utf-8');
const rootCert = pki.certificateFromPem(rootCertPem);

// openssl genrsa -out key.pem 2048
// openssl rsa -in key.pem -outform PEM -pubout -out public.pem
const clientPublicKey = pki.publicKeyFromPem(Buffer.from(event.certPubkey, 'base64').toString('utf-8'));

// Create a client certificate signing request (CSR)
const clientCertReq = pki.createCertificationRequest();
clientCertReq.publicKey = clientPublicKey;
clientCertReq.setSubject([
{ name: 'countryName', value: 'US' },
{ name: 'localityName', value: 'California' },
{ name: 'organizationName', value: 'Fundwave' },
{ name: 'organizationalUnitName', value: 'Fundwave' },
{ name: 'commonName', value: roleName }
]);

// Sign the client certificate request with the root certificate and private key
const clientCert = pki.createCertificate();
clientCert.publicKey = clientCertReq.publicKey;
clientCert.serialNumber = '01'; // Set a unique serial number

const startDate = new Date(); // Valid from the current date and time
const endDate = new Date();
endDate.setDate(startDate.getDate() + event.certValidity);
clientCert.validity.notBefore = startDate;
clientCert.validity.notAfter = endDate;

clientCert.setSubject(clientCertReq.subject.attributes);
clientCert.setIssuer(rootCert.subject.attributes);
clientCert.setExtensions([
{ name: 'basicConstraints', cA: false },
{ name: 'keyUsage', digitalSignature: true, nonRepudiation: true, keyEncipherment: true },
]);
clientCert.sign(rootKey, md.sha256.create());

// Convert the signed client certificate to PEM format
const clientCertPem = pki.certificateToPem(clientCert);
return {
statusCode: 200,
body: Buffer.from(clientCertPem).toString('base64')
};
}
31 changes: 31 additions & 0 deletions private-ca/lambda/generate-host-ssh-cert.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import fs from 'fs';
import child_process from 'child_process';
import util from 'util';

const exec = util.promisify(child_process.exec);

export const signHostSSHCertificate = async (callerIdentity, secret, certValidity, certPubkey) => {

const arn = callerIdentity.GetCallerIdentityResponse.GetCallerIdentityResult.Arn;
const roleName = arn.match(/\/([^/]+)$/)?.[1];

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');
certPubkey = Buffer.from(certPubkey, 'base64').toString('utf-8');
fs.writeFileSync(caKeyPath, host_ca);
fs.writeFileSync(publicKeyPath, certPubkey);

let { stdout, stderr } = await exec(`chmod 600 ${caKeyPath}`);
console.log('stdout:', stdout);
console.log('stderr:', stderr);

({ stdout, stderr } = await exec(`ssh-keygen -s ${caKeyPath} -I host_${roleName} -h -n ${roleName} -V +${certValidity}d ${publicKeyPath}`));
console.log('stdout:', stdout);
console.log('stderr:', stderr);

const certificate = fs.readFileSync(certificatePath, 'utf8');
return certificate;
};
Loading