How to use they private key of a GitHub App when stored in an Azure Key Vault as sign-only? #141974
Replies: 6 comments 4 replies
-
|
storing a private key in Azure Key Vault as "sign-only" means it cannot be accessed directly, only used to sign requests. You don't need a second location to store the key. Instead, utilize Azure's Key Vault to sign the JWT without retrieving the key. The GitHub App can send the payload to Azure Key Vault, which will return the signed JWT. This approach avoids exposing the private key and aligns with best practices for security. Let me know if you'd like any more details or clarification! |
Beta Was this translation helpful? Give feedback.
-
|
You can use Azure Key Vault to securely store and retrieve your private key for generating a JWT for your GitHub app. Below is a Python snippet that demonstrates how to retrieve a private key from Azure Key Vault and generate a JWT using the pip install azure-identity azure-keyvault-secrets pyjwtHere’s the Python code: import jwt
import datetime
from azure.identity import DefaultAzureCredential
from azure.keyvault.secrets import SecretClient
# Constants
KEY_VAULT_NAME = "YourKeyVaultName"
SECRET_NAME = "YourPrivateKeySecretName"
GITHUB_APP_ID = "YourGitHubAppID"
# Authenticate and create a Key Vault client
credential = DefaultAzureCredential()
key_vault_url = f"https://{KEY_VAULT_NAME}.vault.azure.net/"
client = SecretClient(vault_url=key_vault_url, credential=credential)
# Retrieve the private key from Azure Key Vault
private_key = client.get_secret(SECRET_NAME).value
# Generate JWT
now = datetime.datetime.utcnow()
payload = {
'iat': now,
'exp': now + datetime.timedelta(minutes=10), # Token valid for 10 minutes
'iss': GITHUB_APP_ID,
}
jwt_token = jwt.encode(payload, private_key, algorithm='RS256')
print("Generated JWT:", jwt_token)Explanation:
Note:
|
Beta Was this translation helpful? Give feedback.
-
|
When you want to use Azure Key Vault to handle your private key securely, here's what you need to know: Storing the Private Key: You should save the private key as a secret in Azure Key Vault. This keeps it safe and ensures that it’s not easily accessible. Using the Private Key: When you need to sign something, like a JWT: Instead of pulling the private key out of the vault, your application makes a request to Azure Key Vault, asking it to sign your data. |
Beta Was this translation helpful? Give feedback.
-
|
I'm really glad the explanation helped clarify things for you! Since the insights addressed the confusion you were experiencing and helped clear up the misunderstanding, it would be great if you could consider marking my response as the accepted answer. This could also help others who might face a similar issue and find the solution more quickly. Of course, if you still need any further clarification or assistance with your code snippet, feel free to reach out. I'd be happy to help! 😊 |
Beta Was this translation helpful? Give feedback.
-
|
As an addendum to the marked answer, I've set up a code sample that achieves the generation of a JWT for a GitHub App in Python with an Azure Key Vault key and the "sign" operation. It is meant to be compared to the official Python example from their documentation side-by-side. #!/usr/bin/env python3
import base64
import hashlib
import json
import sys
import time
import httpx
# Azure SDK
from azure.identity import DefaultAzureCredential
from azure.keyvault.keys import KeyClient
from azure.keyvault.keys.crypto import CryptographyClient
# Extension of example: https://docs.github.com/en/apps/creating-github-apps/authenticating-with-a-github-app/generating-a-json-web-token-jwt-for-a-github-app#example-using-python-to-generate-a-jwt
# Get the key vault url
if len(sys.argv) > 1:
key_vault_url = sys.argv[1]
else:
key_vault_url = input("Enter url of key vault: ")
# Get the key name
if len(sys.argv) > 2:
key_name = sys.argv[2]
else:
key_name = input("Enter name of key: ")
# Get the Client ID
if len(sys.argv) > 3:
client_id = sys.argv[3]
else:
client_id = input("Enter your GitHub App Client ID: ")
payload = {
# Issued at time in the past to avoid clock skew
'iat': int(time.time() - 60),
# JWT expiration time (10 minutes maximum)
'exp': int(time.time()) + 600,
# GitHub App's client ID
'iss': client_id
}
# This cannot be used since we don't have access to the private key directly
# encoded_jwt = jwt.encode(payload, signing_key, algorithm='RS256')
# The next sections basically replace the jwt.encode() call that uses the private key directly
# To create a valid JWT we need the header, playload, and as url-safe base64 encoded strings separated by a dot (.).
# For more information see https://jwt.io/introduction
# Encode the payload as base64
payload_str = json.dumps(payload)
payload_base64str = base64.urlsafe_b64encode(
payload_str.encode('utf-8')).decode('ascii').strip("=")
# Encode the header as base64
header = {
'alg': 'RS256',
'typ': 'JWT'
}
header_str = json.dumps(header)
header_base64str = base64.urlsafe_b64encode(
header_str.encode('utf-8')).decode('ascii').strip("=")
# Concatenate the header and payload
headerAndPayload = f"{header_base64str}.{payload_base64str}"
# Sign the header and payload via the the Azure Key Vault key
# Hash the header and payload
digest = hashlib.sha256(headerAndPayload.encode('utf-8')).digest()
key_client = KeyClient(vault_url=key_vault_url,
credential=DefaultAzureCredential())
az_key = key_client.get_key(key_name)
crypto_client = CryptographyClient(
az_key, DefaultAzureCredential())
signature = crypto_client.sign('RS256', digest).signature
# Encode the signature as base64
sigature_base64str = base64.urlsafe_b64encode(
signature).decode('ascii').strip("=")
# Concatenate the header, payload and signature to get a valid JWT
encoded_jwt = f"{headerAndPayload}.{sigature_base64str}"
# call API that allows JWT authentication
# e.g.: https://docs.github.com/en/rest/apps/apps?apiVersion=2022-11-28#list-installations-for-the-authenticated-app
httpx_client = httpx.Client()
response = httpx_client.get(f"https://api.github.com/app/installations",
headers={
"Authorization": f"Bearer {encoded_jwt}",
"Accept": "application/vnd.github+json",
"X-GitHub-Api-Version": "2022-11-28"
})
# Expected output: "Request returned status code: 200"
print(f"Request returned status code: {response.status_code}")This works for generating a valid JWT (which can the be used for any subsequent authentication process). However, my remaining main gripe with this solution is that it's more or less "unsupported" by the official and community GitHub SDKs I've looked into so far (pygithub and octokit.js - specifically tracking this issue right now). Personally, I'll proceed with storing the key in the Azure Key Vault as a secret for now and accessing it directly in order to keep using the SDKs. Would love to see more opinions around this though 😉 PS: If anyone wants to improve my implementation to generate a JWT with less overhead please feel free to iterate on this. Anything "encryption" is basically new territory for me... |
Beta Was this translation helpful? Give feedback.
-
|
It is also worth noting that this can be done with az keyvault key sign likely from a recent fix in this space. |
Beta Was this translation helpful? Give feedback.
Uh oh!
There was an error while loading. Please reload this page.
-
Select Topic Area
Question
Body
Based on the best practice for creating GitHub Apps, it's suggested to store it as sign-only key:
However, there are no options to get access to the private key once stored there which is a requirement to generate a JWT in the next step (reference). In my mind, this can only be achieved by storing the private key at a "second" location where the app can actually access its content (e.g. as an Azure Key Vault secret).
Thanks a lot in advance to anyone helping out here 😊
Beta Was this translation helpful? Give feedback.
All reactions