Connect an infrastructure-as-code program¶
A Pulumi or Terraform program that manages a GitHub organisation acts as a catalogue GitHub App, the iac App in the default set. It does not act as a person or as sluis. The program owns structure: repositories, teams, rulesets and settings. sluis owns the identities and credentials.
Before you start¶
-
Prefer the exchange (mint a token). Use the store only when an apply must run while sluis is upgraded, replaced or restored.
-
The store holds the App's private key. Whoever reads it acts as the App, outside the audit trail.
-
pushneedsconfig.store: kubernetesand an External Secrets store you already have.
| From a secret store | Exchanged at run time | |
|---|---|---|
| Program reads | a path in OpenBao, Vault or a cloud manager | an installation token from /token |
| Credential | the App's private key, durable | a token of at most an hour |
| Audit trail | the store's own log | one roster.github_token.minted record per run |
Steps¶
1. Declare the App and its push¶
githubApps:
catalogue:
- id: iac
org: example-org
description: The program that manages the organisation
permissions:
organization_administration: write
members: write
administration: write
contents: read
metadata: read
installation: all
push:
secretStore:
name: example-store
kind: ClusterSecretStore # SecretStore (default) | ClusterSecretStore
remoteKey: platform/github-apps/iac
refreshInterval: 1h # optional
deletionPolicy: None # optional; None (default) | Delete
push is off unless an entry carries it. The chart renders one External Secrets PushSecret per entry, <release>-github-app-<id>. It copies three keys and nothing else:
In the store at remoteKey |
From | Is |
|---|---|---|
app_id |
<id>.github_app_id |
the App's numeric id |
installation_id |
<id>.github_app_installation_id |
its installation on the organisation |
private_key |
<id>.github_app_private_key |
the App's private key, PEM |
The keys exist once the App is installed. Two entries pushing to one path in one store are refused at render. Roll out the release.
2. Create and install¶
On the console's GitHub page, Apps tab, press Create on the App. An owner confirms GitHub's manifest, then installs on the organisation. The service writes the keys and the PushSecret copies them within its refresh interval. Roll back by removing the entry.
3. Read it in the program¶
Pulumi in Go:
package main
import (
"github.com/pulumi/pulumi-github/sdk/v6/go/github"
"github.com/pulumi/pulumi-vault/sdk/v6/go/vault/kv"
"github.com/pulumi/pulumi/sdk/v3/go/pulumi"
)
func main() {
pulumi.Run(func(ctx *pulumi.Context) error {
app, err := kv.LookupSecretV2(ctx, &kv.LookupSecretV2Args{
Mount: "kv",
Name: "platform/github-apps/iac",
})
if err != nil {
return err
}
// Mark the key secret, or the stack state holds it in the clear.
pem := pulumi.ToSecret(pulumi.String(app.Data["private_key"])).(pulumi.StringOutput)
gh, err := github.NewProvider(ctx, "github", &github.ProviderArgs{
Owner: pulumi.String("example-org"),
AppAuth: &github.ProviderAppAuthArgs{
Id: pulumi.String(app.Data["app_id"]),
InstallationId: pulumi.String(app.Data["installation_id"]),
PemFile: pem,
},
})
if err != nil {
return err
}
_, err = github.NewRepository(ctx, "docs", &github.RepositoryArgs{
Name: pulumi.String("docs"),
Visibility: pulumi.String("private"),
}, pulumi.Provider(gh))
return err
})
}
Terraform:
data "vault_kv_secret_v2" "iac" {
mount = "kv"
name = "platform/github-apps/iac"
}
provider "github" {
owner = "example-org"
app_auth {
id = data.vault_kv_secret_v2.iac.data["app_id"]
installation_id = data.vault_kv_secret_v2.iac.data["installation_id"]
pem_file = data.vault_kv_secret_v2.iac.data["private_key"]
}
}
Terraform state holds data-source results in the clear, so use an encrypted backend. Keep the PEM's newlines: a flattened key fails with could not parse private key.
4. Use the exchange in CI instead¶
A job that can reach the issuer should not read the store:
permissions:
id-token: write
steps:
- id: access
uses: truvity/sluis@<commit-sha> # vX.Y.Z
with:
issuer: https://access.example.com
github-app: ci-automation
repositories: docs
permissions: pull_requests:write
- run: gh pr review --approve "$PR"
env:
GH_TOKEN: ${{ steps.access.outputs.github-token }}
A laptop or script uses sluisctl github-token.
Rotate and remove¶
-
GitHub has no API to replace an App's key. To rotate, press Disconnect on the App's page and create the App again. The new key goes to the same path.
-
A key generated in the App's GitHub settings invalidates sluis's copy too.
-
Give the path its own store policy.
-
deletionPolicy: Noneleaves the copy when you remove the entry.Deleteremoves it. -
Push only Apps whose consumers cannot exchange a token.
To back up the whole Secret, see backing it up.