This action uses GitHub's OIDC support to authenticate towards a HashiCorp Vault instance or an OpenBao instance, and to request a (short-lived) SSH client certificate from it.
jobs:
deploy:
permissions:
contents: read
id-token: write
# ...
steps:
# ...
- name: Generate SSH client certificate
if: github.ref == 'refs/heads/main'
id: ssh_cert
uses: andreaso/vault-oidc-ssh-cert-action@v1
with:
vault_server: https://vault.example.com:8200
jwt_audience: vault.example.com
jwt_oidc_backend_path: github-oidc
jwt_oidc_role: example-user
ssh_backend_path: ssh-client-ca
ssh_role: github-actions-example
- name: Deploy site
if: github.ref == 'refs/heads/main'
run: >
rsync -e "ssh -i '$SSH_KEY_PATH'"
--verbose --recursive --delete-after --perms --chmod=D755,F644
build/ deployer@site.example.net:/var/www/site/
env:
SSH_KEY_PATH: ${{ steps.ssh_cert.outputs.key_path }}
Do note that all client certification configuration is expected to happen on the Vault end, given that that is where all the limitations can be enforced.
All the action's writes are to the ${{ runner.temp }}
directory. Hence as soon as the job is completed both the SSH
certificate and its private key will be automatically removed, even in
the case of a non-ephemeral runner.
resource "vault_jwt_auth_backend" "github" {
path = "github-oidc"
oidc_discovery_url = "https://token.actions.githubusercontent.com"
bound_issuer = "https://token.actions.githubusercontent.com"
}
resource "vault_mount" "ssh_ca" {
path = "ssh-client-ca"
type = "ssh"
}
resource "vault_ssh_secret_backend_ca" "ssh_ca" {
backend = vault_mount.ssh_ca.path
key_type = "ed25519"
generate_signing_key = true
}
resource "vault_ssh_secret_backend_role" "example" {
name = "github-actions-example"
backend = vault_mount.ssh_ca.path
max_ttl = "900"
key_type = "ca"
allow_user_certificates = true
allow_host_certificates = false
allowed_users = "github-deploy@example.com"
default_user = "github-deploy@example.com"
default_extensions = {}
allowed_user_key_config {
type = "ed25519"
lengths = [0]
}
}
data "vault_policy_document" "example" {
rule {
path = "${vault_mount.ssh_ca.path}/sign/${vault_ssh_secret_backend_role.example.name}"
capabilities = ["update"]
}
}
resource "vault_policy" "example" {
name = "example-policy"
policy = data.vault_policy_document.example.hcl
}
resource "vault_jwt_auth_backend_role" "example" {
backend = vault_jwt_auth_backend.github.path
role_type = "jwt"
role_name = "example-user"
token_max_ttl = "300"
token_policies = [vault_policy.example.name]
user_claim = "actor"
bound_audiences = ["vault.example.com"]
bound_claims = {
repository = "OWNER/REPO-NAME",
ref = "refs/heads/main",
}
}
output "ssh_ca" {
value = vault_ssh_secret_backend_ca.ssh_ca.public_key
}
# /etc/ssh/sshd_config
# ...
TrustedUserCAKeys /etc/ssh/sshd_user_ca.pub
AuthorizedPrincipalsFile /etc/ssh/user_principals/%u
# /etc/ssh/sshd_user_ca.pub
ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAI...
# /etc/ssh/user_principals/deployer
github-deploy@example.com