diff --git a/EXAMPLES.md b/EXAMPLES.md new file mode 100644 index 0000000..dade231 --- /dev/null +++ b/EXAMPLES.md @@ -0,0 +1,192 @@ +# Examples + +Usage examples for exchanging OIDC tokens from various identity providers via **github-sts**. + +--- + +## Azure AD (Entra ID) + +### Prerequisites + +- An Azure AD App Registration configured as a federated identity provider +- The Azure AD tenant OIDC issuer added to your github-sts configuration: + +```yaml +oidc: + allowed_issuers: + - "https://sts.windows.net//" +``` + +- A trust policy (e.g. `.github/sts/default/my-azure-identity.sts.yaml`) in the target repository that matches Azure AD token claims + +--- + +### Local / CLI Usage + +Log in with an Azure AD service principal and exchange the resulting access token for a scoped GitHub token: + +```bash +# Authenticate with Azure AD using a service principal +az login --service-principal \ + --username \ + --password \ + --tenant \ + --allow-no-subscriptions + +# Obtain an OIDC access token +OIDC_TOKEN=$(az account get-access-token --query accessToken -o tsv) + +# Exchange the OIDC token for a scoped GitHub installation token +curl -sf \ + -H "Authorization: Bearer ${OIDC_TOKEN}" \ + "https://github-sts.example.com/sts/exchange?scope=my-org/my-repo&app=default&identity=my-azure-identity" +``` + +**Example response:** + +```json +{ + "token": "ghs_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx", + "scope":"/", + "app":"default", + "identity":"test-azure", + "permissions": { + "contents": "read" + }, + "expires_at": "2026-03-09T12:00:00Z" +} +``` + +--- + +### GitHub Actions + +Use Azure federated credentials with OIDC to obtain a scoped GitHub token inside a workflow: + +```yaml +name: Azure STS Example + +on: + workflow_dispatch: + inputs: + repository: + description: "Target repository (org/repo)" + required: true + identity: + description: "STS identity name" + required: true + +permissions: + id-token: write # Required for Azure OIDC + +jobs: + azure-sts: + runs-on: ubuntu-latest + steps: + - name: Install Azure CLI + run: | + curl -sL https://aka.ms/InstallAzureCLIDeb | sudo bash + + - name: Azure Login (OIDC) + uses: azure/login@v2 + with: + client-id: ${{ secrets.AZ_APP_ID }} + tenant-id: ${{ secrets.AZ_TENANT_ID }} + allow-no-subscriptions: true + + - name: Get scoped GitHub token via STS + id: sts + run: | + OIDC_TOKEN=$(az account get-access-token --query accessToken -o tsv) + + GITHUB_TOKEN=$(curl -sf \ + -H "Authorization: Bearer ${OIDC_TOKEN}" \ + "https://github-sts.example.com/sts/exchange?scope=${{ inputs.repository }}&app=default&identity=${{ inputs.identity }}" \ + | jq -r '.token') + + echo "::add-mask::$GITHUB_TOKEN" + echo "token=$GITHUB_TOKEN" >> $GITHUB_OUTPUT + + - name: Checkout target repository + uses: actions/checkout@v4 + with: + repository: ${{ inputs.repository }} + token: ${{ steps.sts.outputs.token }} + path: external-repo +``` + +**Required repository secrets:** + +| Secret | Description | +|---|---| +| `AZ_APP_ID` | Azure AD Application (client) ID | +| `AZ_TENANT_ID` | Azure AD Tenant ID | + +> **Note:** No client secret is needed in the GitHub Actions workflow when using [Azure Workload Identity Federation](https://learn.microsoft.com/en-us/entra/workload-id/workload-identity-federation) with GitHub's OIDC provider. + +--- + +## GitHub Actions (Native OIDC) + +GitHub Actions can issue OIDC tokens natively — no external identity provider required. The workflow requests an OIDC token directly from GitHub's token endpoint and exchanges it with github-sts. + +### Prerequisites + +- The GitHub Actions OIDC issuer added to your github-sts configuration: + +```yaml +oidc: + allowed_issuers: + - "https://token.actions.githubusercontent.com" +``` + +- A trust policy (e.g. `.github/sts/default/my-identity.sts.yaml`) in the target repository that matches GitHub Actions token claims +- The workflow must have `id-token: write` permission + +--- + +### GitHub Actions Workflow + +```yaml +name: GitHub Actions STS Example + +on: + workflow_dispatch: + inputs: + repository: + description: "Target repository (org/repo)" + required: true + identity: + description: "STS identity name" + required: true + +permissions: + id-token: write # Required to request the OIDC token + +jobs: + github-sts: + runs-on: ubuntu-latest + steps: + - name: Get scoped GitHub token via STS + id: sts + run: | + OIDC_TOKEN=$(curl -sH "Authorization: bearer $ACTIONS_ID_TOKEN_REQUEST_TOKEN" \ + "$ACTIONS_ID_TOKEN_REQUEST_URL&audience=github-sts" | jq -r '.value') + + GITHUB_TOKEN=$(curl -sf \ + -H "Authorization: Bearer $OIDC_TOKEN" \ + "https://github-sts.example.com/sts/exchange?scope=${{ inputs.repository }}&app=default&identity=${{ inputs.identity }}" \ + | jq -r '.token') + + echo "::add-mask::$GITHUB_TOKEN" + echo "token=$GITHUB_TOKEN" >> $GITHUB_OUTPUT + + - name: Checkout target repository + uses: actions/checkout@v4 + with: + repository: ${{ inputs.repository }} + token: ${{ steps.sts.outputs.token }} + path: external-repo +``` + +> **Note:** No secrets are needed — GitHub Actions provides the `ACTIONS_ID_TOKEN_REQUEST_TOKEN` and `ACTIONS_ID_TOKEN_REQUEST_URL` environment variables automatically when `id-token: write` is set. diff --git a/NOTICE.md b/NOTICE.md new file mode 100644 index 0000000..02770e9 --- /dev/null +++ b/NOTICE.md @@ -0,0 +1,11 @@ +This project is inspired by octo-sts/app (https://github.com/octo-sts/app), +an excellent Go-based implementation that pioneered the concept of using OIDC +federation for GitHub token exchange. + +octo-sts/app is licensed under the Apache License, Version 2.0: +https://github.com/octo-sts/app/blob/main/LICENSE + +While github-sts is an independent Python implementation and does not share +code with octo-sts/app, the core concept of exchanging OIDC tokens for scoped +GitHub installation tokens — including the trust policy model and the +repository-based policy resolution pattern — originated from that project. diff --git a/README.md b/README.md index cc16c79..6728018 100644 --- a/README.md +++ b/README.md @@ -2,41 +2,20 @@ A Python-based Security Token Service (STS) for the GitHub API. -Workloads with OIDC tokens (GitHub Actions, GCP, AWS, Kubernetes, Okta, …) exchange them for **short-lived, scoped GitHub installation tokens**. No PATs required. +Workloads with OIDC tokens (GitHub Actions, Azure AD, GCP, AWS, Kubernetes, Okta, …) exchange them for **short-lived, scoped GitHub installation tokens**. No PATs required. Supports **multiple GitHub Apps** with YAML-based configuration (ideal for Kubernetes ConfigMaps). -Inspired by [**octo-sts/app**](https://github.com/octo-sts/app), an excellent Go-based implementation that pioneered the concept of using OIDC federation for GitHub token exchange. +Inspired by [**octo-sts/app**](https://github.com/octo-sts/app) — see [NOTICE](NOTICE) for attribution. --- -## Why? - -Organizations using GitHub often face a dilemma when distributing access: - -| Approach | Pros | Cons | -|---|---|---| -| **GitHub App Tokens** | Secure, scoped | Complex to manage across many systems | -| **Personal Access Tokens (PATs)** | Simple | Long-lived, broad permissions | -| **Deploy Keys** | Scoped to repos | Read-only, hard to rotate | -| **SSH Keys** | Familiar | Hard to audit, tied to individuals | - -**github-sts** eliminates the tradeoff — workloads present an OIDC token and receive a short-lived, least-privilege GitHub token with no stored credentials. +## How It Works -**CI/CD & Automation:** ``` -Workflow → OIDC Token → STS → Scoped GitHub Token → Deploy +Workload → OIDC Token → github-sts → Scoped GitHub Token ``` -**Internal Tools & Scripts:** -``` -Developer Tool → OIDC Token (from corporate IdP) → STS → Temporary Access -``` - ---- - -## How It Works - ``` Workload github-sts GitHub │ │ │ @@ -48,8 +27,6 @@ Developer Tool → OIDC Token (from corporate IdP) → STS → Temporary Access │─────────────────────────>│ │ │ │ Validate OIDC sig/exp │ │ │ Load trust policy │ - │ │ {base_path}/{app}/ │ - │ │ {identity}.sts.yaml │ │ │ Evaluate claims │ │ │ Request install token ──> │ │<─────────────────────────│ @@ -59,115 +36,80 @@ Developer Tool → OIDC Token (from corporate IdP) → STS → Temporary Access --- -## Project Structure - -``` -src/github_sts/ # Main package - ├── main.py # FastAPI app entry point - ├── config.py # YAML + env var configuration - ├── policy.py # Trust policy model & validation - ├── oidc.py # OIDC token validation & verification - ├── github_app.py # GitHub App token provider - ├── policy_loader.py # Policy storage backends - ├── jti_cache.py # JTI validation caching - ├── metrics.py # Prometheus metrics - ├── audit.py # Request auditing - └── routes/ # API endpoints - ├── exchange.py # Token exchange endpoint - └── health.py # Health check endpoints - -tests/ # Test suite - ├── test_policy.py # Trust policy tests - ├── test_audit.py # Audit logging tests - └── test_jti_cache.py # JTI cache tests - -pyproject.toml # Project configuration (dependencies, tools) -Dockerfile # Multi-stage Docker build -``` - ---- - ## Quick Start -### Prerequisites - -- Python 3.14+ -- [uv](https://docs.astral.sh/uv/) (recommended) or pip +### Option 1: Docker (local only) -### 1. Get GitHub App Credentials +A pre-built image is available from [GitHub Container Registry](https://github.com/AlexandreODelisle/py-github-sts/pkgs/container/py-github-sts): ```bash -export PYGITHUBSTS_GITHUB_APP_ID=$(vault kv get -field=github_app_id homelab/github-action/github-sts) -export PYGITHUBSTS_GITHUB_APP_PRIVATE_KEY=$(vault kv get -field=github_app_private_key homelab/github-action/github-sts) +docker run -p 9999:8080 \ + -e PYGITHUBSTS_GITHUB_APP_ID="your_app_id" \ + -e PYGITHUBSTS_GITHUB_APP_PRIVATE_KEY="$(cat /path/to/private_key.pem)" \ + ghcr.io/alexandreodelisle/py-github-sts:latest ``` -Or create a config file (see [config/github-sts.example.yaml](config/github-sts.example.yaml)): -```bash -export PYGITHUBSTS_CONFIG_PATH=./config/github-sts.example.yaml -export PYGITHUBSTS_GITHUB_APP_ID=your_app_id -export PYGITHUBSTS_GITHUB_APP_PRIVATE_KEY=your_private_key -``` +### Option 2: Helm (Kubernetes) -### 2. Install Dependencies +The Helm chart is published to the GitHub Container Registry OCI repository: -**Using uv (recommended):** ```bash -uv sync -``` +# Create credentials secret +kubectl create secret generic github-sts-credentials \ + --from-literal=github-app-id="YOUR_GITHUB_APP_ID" \ + --from-file=github-app-private-key=/path/to/private_key.pem -**Using pip:** -```bash -pip install -e . +# Install from OCI registry +helm install github-sts \ + oci://ghcr.io/alexandreodelisle/py-github-sts/github-sts-chart \ + --set github.existingSecret="github-sts-credentials" ``` -### 3. Run Locally +See the [chart README](charts/github-sts/README.md) for full configuration options including Ingress/HTTPRoute setup. -**With uv:** -```bash -uv run python -m uvicorn github_sts.main:app --host 0.0.0.0 --port 9999 -``` +### Option 3: From Source -**With pip:** ```bash -python -m uvicorn github_sts.main:app --host 0.0.0.0 --port 9999 -``` +# Prerequisites: Python 3.14+, uv (https://docs.astral.sh/uv/) +uv sync -**With auto-reload (development):** -```bash -uv run python -m uvicorn github_sts.main:app --host 0.0.0.0 --port 9999 --reload +export PYGITHUBSTS_GITHUB_APP_ID=your_app_id +export PYGITHUBSTS_GITHUB_APP_PRIVATE_KEY="$(cat /path/to/private_key.pem)" + +uv run python -m uvicorn github_sts.main:app --host 0.0.0.0 --port 9999 ``` -### 4. Test the Service +### Verify -Health check: ```bash curl http://localhost:9999/health # {"status":"ok"} ``` -Exchange a token: +--- + +## Usage + +Exchange an OIDC token for a scoped GitHub token: + ```bash -curl -H "Authorization: Bearer $OIDC_TOKEN" \ +curl -sf -H "Authorization: Bearer $OIDC_TOKEN" \ "http://localhost:9999/sts/exchange?scope=org/repo&app=default&identity=ci" ``` -View metrics: -```bash -curl http://localhost:9999/metrics -``` +For complete usage examples — including GitHub Actions (native OIDC), Azure AD / Entra ID (CLI and workflows), and more — see **[EXAMPLES.md](EXAMPLES.md)**. --- ## Trust Policies -Policies are fetched directly from GitHub repositories. +Policies are fetched directly from GitHub repositories at: -Each policy lives at `{base_path}/{app_name}/{identity}.sts.yaml` in the target repository. - -Default path: `.github/sts/{app_name}/{identity}.sts.yaml` +``` +{base_path}/{app_name}/{identity}.sts.yaml +``` -For example, with `app=my-app` and `identity=ci`: -`.github/sts/my-app/ci.sts.yaml` +Default: `.github/sts/default/{identity}.sts.yaml` ### Policy Schema @@ -182,10 +124,10 @@ permissions: **Regex patterns (flexible):** ```yaml -issuer: https://accounts.google.com -subject_pattern: "[0-9]+" # Google SA unique ID +issuer: https://login.microsoftonline.com//v2.0 +subject_pattern: "[a-f0-9-]+" # Azure AD object ID claim_pattern: - email: ".*@example\\.com" # restrict by email domain + azp: "" # restrict by app registration permissions: contents: read ``` @@ -213,45 +155,11 @@ permissions: --- -## GitHub Actions Usage - -```yaml -jobs: - deploy: - runs-on: ubuntu-latest - permissions: - id-token: write - steps: - - name: Get scoped GitHub token - id: sts - run: | - OIDC_TOKEN=$(curl -sH "Authorization: bearer $ACTIONS_ID_TOKEN_REQUEST_TOKEN" \ - "$ACTIONS_ID_TOKEN_REQUEST_URL&audience=github-sts" | jq -r '.value') - - GITHUB_TOKEN=$(curl -sf \ - -H "Authorization: Bearer $OIDC_TOKEN" \ - "${{ vars.STS_URL }}/sts/exchange?scope=${{ github.repository }}&app=default&identity=ci" \ - | jq -r '.token') - - echo "::add-mask::$GITHUB_TOKEN" - echo "token=$GITHUB_TOKEN" >> $GITHUB_OUTPUT - - - name: Use scoped token - env: - GITHUB_TOKEN: ${{ steps.sts.outputs.token }} - run: gh issue list -``` - ---- - ## Configuration -### Configuration File (YAML) - github-sts uses YAML-based configuration, ideal for Kubernetes ConfigMaps. See [config/github-sts.example.yaml](config/github-sts.example.yaml) for a complete example. -Set the config file path: ```bash export PYGITHUBSTS_CONFIG_PATH=/etc/github-sts/config.yaml ``` @@ -260,18 +168,6 @@ export PYGITHUBSTS_CONFIG_PATH=/etc/github-sts/config.yaml Environment variables with `PYGITHUBSTS_` prefix override YAML config. -**Single-app shortcut (env vars):** -```bash -export PYGITHUBSTS_GITHUB_APP_ID=your_app_id -export PYGITHUBSTS_GITHUB_APP_PRIVATE_KEY=your_private_key -``` - -**From Vault:** -```bash -export PYGITHUBSTS_GITHUB_APP_ID=$(vault kv get -field=github_app_id homelab/github-action/github-sts) -export PYGITHUBSTS_GITHUB_APP_PRIVATE_KEY=$(vault kv get -field=github_app_private_key homelab/github-action/github-sts) -``` - | Env var | Default | Description | |---|---|---| | `PYGITHUBSTS_CONFIG_PATH` | — | Path to YAML config file | @@ -310,47 +206,6 @@ export PYGITHUBSTS_GITHUB_APP_PRIVATE_KEY=$(vault kv get -field=github_app_priva --- -## Docker - -### Build the image - -```bash -docker build -t github-sts:latest . -``` - -### Run with Docker - -```bash -docker run -p 9999:8080 \ - -e PYGITHUBSTS_GITHUB_APP_ID="$PYGITHUBSTS_GITHUB_APP_ID" \ - -e PYGITHUBSTS_GITHUB_APP_PRIVATE_KEY="$PYGITHUBSTS_GITHUB_APP_PRIVATE_KEY" \ - github-sts:latest -``` - -Service will be available at `http://localhost:9999` - ---- - -## Helm Chart - -A Helm chart is available for Kubernetes deployments in [`charts/github-sts`](charts/github-sts). - -### Basic Deployment - -```bash -# Create credentials secret -kubectl create secret generic github-sts-credentials \ - --from-literal=github-app-id="YOUR_GITHUB_APP_ID" \ - --from-file=github-app-private-key=/path/to/private_key.pem - -# Install -helm install github-sts ./charts/github-sts \ - --set github.existingSecret="github-sts-credentials" -``` - -See the [chart README](charts/github-sts/README.md) for full configuration options, Ingress/HTTPRoute setup, and more examples. - - ## Development ### Linting & Formatting @@ -410,49 +265,6 @@ The project uses **Ruff** for linting and formatting: Configuration is in `pyproject.toml` under `[tool.ruff]` -### Using Make (convenience commands) - -```bash -make dev # Install dependencies -make lint # Run linter -make format # Format code -make check # Run all checks -make test # Run tests -make clean # Clean cache files -``` - ---- - -## Troubleshooting - -### "No module named 'src'" -Make sure you've installed the package: -```bash -uv sync -# or -pip install -e . -``` - -### "ruff: command not found" -Initialize the uv environment: -```bash -uv sync -uv run ruff --version -``` - -### Tests fail with import errors -Reinstall the package in development mode: -```bash -uv sync -uv run pytest -``` - -### Health check fails -Verify environment variables are set: -```bash -env | grep PYGITHUBSTS -``` - --- ## Contributing @@ -474,11 +286,18 @@ MIT License — See [LICENSE](LICENSE) ## References +**Inspiration:** - [octo-sts/app](https://github.com/octo-sts/app) — Original Go implementation -- [GitHub OIDC Documentation](https://docs.github.com/en/actions/deployment/security-hardening-your-deployments/about-security-hardening-with-openid-connect) + +**GitHub:** +- [GitHub Apps](https://docs.github.com/en/apps) +- [GitHub Actions OIDC](https://docs.github.com/en/actions/deployment/security-hardening-your-deployments/about-security-hardening-with-openid-connect) + +**Standards:** - [OpenID Connect Specification](https://openid.net/connect/) -- [FastAPI Documentation](https://fastapi.tiangolo.com/) -- [uv Documentation](https://docs.astral.sh/uv/) -- [Ruff Documentation](https://docs.astral.sh/ruff/) -- [pytest Documentation](https://docs.pytest.org/) -- [GitHub App Documentation](https://docs.github.com/en/apps) + +**Tools:** +- [FastAPI](https://fastapi.tiangolo.com/) +- [uv](https://docs.astral.sh/uv/) +- [Ruff](https://docs.astral.sh/ruff/) +- [pytest](https://docs.pytest.org/)