PostHog Handbook Library / Engineering

1,634 words. Estimated reading time: 8 min.

# Security Best Practices

Auto TL;DR

## At a Glance

This long page covers these main areas. The list is generated from the article headings, so it updates with every handbook rebuild.

1. GitHub
2. SSH Keys
3. Setting up with Secretive
4. Setting up with 1Password
5. Commit signing
6. After setup
7. Troubleshooting
8. GitHub Actions

### GitHub

#### SSH Keys

Connecting to GitHub requires an SSH key (unless using HTTPS). Traditional SSH keys live as text files on your filesystem, making them vulnerable to theft or misuse by malware. We explicitly prohibit the use of SSH keys stored on your filesystem.

Use Secretive or 1Password to generate and store your SSH key. We have a slight preference for Secretive because it stores your key in the macOS Secure Enclave, ensuring the key can never be exported or extracted, even by malware. Always use ECDSA or Ed25519 — don't use RSA.

> Note: The default shell on macOS is `zsh`, so the `~/.zshrc` instructions below apply to you unless you've deliberately switched to a different shell. If you're not sure which shell you're using, run `echo $SHELL` in your terminal to check.

##### Setting up with Secretive

1. Open Secretive and click the + button to create a new key.
2. Name your key "GitHub SSH" and select Notify in the Protection Level dropdown.

- For additional protection, select Require Authentication instead. This will require you to use Touch ID each time the key is accessed.

1. Go to Secretive > Integrations in the menu bar.
2. Select your shell on the left side set the `SSH_AUTH_SOCK` environment variable as instructed. For zsh, add the following to your `~/.zshrc`:

       export SSH_AUTH_SOCK=~/Library/Containers/com.maxgoedjen.Secretive.SecretAgent/Data/socket.ssh

Then run `source ~/.zshrc` to apply it.

1. Click on your new key in Secretive and copy the public key.
2. Go to your GitHub SSH keys settings and add a new SSH key. Paste your public key and set the key type to Authentication Key.
3. Test it by running:

       ssh -T git@github.com

You should see a message like "Hi username! You've successfully authenticated".

##### Setting up with 1Password

Follow the 1Password SSH key management guide.

#### Commit signing

A git commit's `Author` field is completely user controllable and can be forged. Signing your commits cryptographically proves you authored them, preventing impersonation and confusion. Signing is required, not optional: an org-wide GitHub ruleset rejects unsigned commits in every PostHog repository, on every branch. Set this up before your first push, rather than when a push fails. This applies to automation too, see signing commits from workflows.

You can sign commits with either Secretive or 1Password. We have a slight preference for Secretive because it stores your key in the macOS Secure Enclave, ensuring the key can never be exported or extracted, even by malware.

##### Setting up with Secretive

1. Open Secretive and click the + button to create a new key.
2. Name your key "Git signing key" and select Notify in the Protection Level dropdown.
3. Go to Secretive > Integrations in the menu bar.
4. Click Git Signing and select "Git signing key" from the Secret dropdown.
5. Copy and paste the `~/.gitconfig` and `~/.gitallowedsigners` snippets into their respective files.

- If you already have content in `~/.gitconfig`, merge the new sections into the existing file rather than replacing it.
- If you've previously configured commit signing (with a different SSH key, a GPG key, or an older Secretive key), replace any existing `signingkey`, `gpgsign`, `gpg.format`, and `allowedSignersFile` entries — don't append duplicates. Git will silently use the last value, but a `.gitconfig` with multiple conflicting `signingkey` lines is hard to reason about. Confirm the active key afterwards with `git config --get user.signingkey`.
- The `~/.gitallowedsigners` file is used by `git log --show-signature` for local verification. Each line is `<your-git-email> <key-type> <public-key>`, e.g. `you@posthog.com ecdsa-sha2-nistp256 AAAA... Git-Signing-Key@...`. If you skip it, signing still works but local verification will report `No principal matched`.

1. Select your shell on the left side of Secretive and set the `SSH_AUTH_SOCK` environment variable as instructed. For zsh, add the following to your `~/.zshrc`:

       export SSH_AUTH_SOCK=~/Library/Containers/com.maxgoedjen.Secretive.SecretAgent/Data/socket.ssh

Then run `source ~/.zshrc` to apply it.

1. Your `~/.gitconfig` now has a `signingkey` pointing to a file. Copy your public key to the clipboard:

       cat <path-from-signingkey> | pbcopy

1. Go to your GitHub SSH keys settings and add a new SSH key. Paste your public key and set the key type to Signing Key.

- GitHub treats Authentication Key and Signing Key as separate roles, even for the same key. If you get "Key is already in use", it most likely means you already added this key as an Authentication Key (from the SSH Keys setup above). The same key can serve both roles — but you have to add it once for each. Add it again with key type Signing Key.

1. Verify the signature locally before pushing. Create an empty commit on a new branch:

       git commit --allow-empty -m "test signing"
       git log -1 --format='%GK %G?'

You should see a fingerprint followed by `G` (good signature). The fingerprint must match the Signing Key you just added on GitHub — find it at GitHub SSH keys settings under the Signing keys heading. If it matches an Authentication key entry instead, your `user.signingkey` in `~/.gitconfig` is pointing at the wrong file — fix it before pushing.

1. Push the branch to GitHub — you should see a green Verified badge on the commit.

Image: Signed commit

##### Setting up with 1Password

Follow the 1Password git commit signing guide.

##### After setup

Once commit signing is configured, enable the option in your GitHub Profile to "Flag unsigned commits as unverified". The org ruleset already blocks unsigned commits in PostHog repos, but this marks any commit attributed to your email and not signed by you as Unverified everywhere else on GitHub, including your personal repos.

##### Troubleshooting

- If using iTerm/Cursor/GitHub Desktop/Sourcetree/etc., you may be endlessly prompted to "access data from other apps". You can fix this by granting the app Full Disk Access in System Settings > Privacy & Security > Full Disk Access.

- If you are prompted to complete Touch ID each time you commit, your signing key is using a Protection Level of Require Authentication. Re-follow the instructions above to generate a new signing key with a Protection Level of Notify.

- GitHub rejects your push with `GH013: Commits must have verified signatures` even though the commits look signed locally. The commit was signed with a key GitHub doesn't recognize as a Signing Key — usually because (a) the key is only registered as an Authentication Key, or (b) your `user.signingkey` points at the wrong file. Confirm with `git log -1 --format='%GK'` and cross-check the fingerprint against your GitHub keys. Once the right key is registered as a Signing Key, re-sign the existing commit:

       git commit --amend --no-edit -S
       git push --force-with-lease

Use `--force-with-lease` rather than plain `--force` — it refuses the push if someone else has pushed to the branch since you last fetched.

#### GitHub Actions

Great care should be taken when writing or modifying a GitHub Actions workflow. Actions can access (and exfiltrate) secrets scoped to the repo. We scan workflows with Semgrep and CodeQL for common misconfigurations.

##### Authentication

Most Actions use the default `GITHUB_TOKEN`, whose permissions can be scoped via the `permissions` property. However, `GITHUB_TOKEN` cannot trigger other workflows, so commits or PRs created by an Action won't run CI, leaving PRs unmergeable without manual intervention. The workaround is a GitHub App.

Personal access tokens are not an option here. Classic PATs are blocked org-wide. Fine-grained PATs require approval and are generally not approved, because they are tied to an individual user and break when that user leaves PostHog.

Use a finely scoped, purpose-specific GitHub App instead. Scope each App to its use case and ideally a single repo. Prefer creating a new App over expanding an existing one's permissions, otherwise every Action using that App inherits permissions it doesn't need.

Send a message in #team-security if you need help setting up a new GitHub App.

##### Signing commits from workflows

The org ruleset applies to Actions too. A workflow that commits with `git push` is rejected, because there is no signing key on the runner. Create the commit through the GitHub API instead, which GitHub signs with its own key. In practice that means one of:

- `planetscale/ghcommit-action`, a wrapper around the GraphQL `createCommitOnBranch` mutation. This is what most of our workflows use.
- `peter-evans/create-pull-request` with `sign-commits: true` (v6.1 or later)
- `changesets/action` with `commitMode: github-api`
- calling `createCommitOnBranch` yourself

> Signing only works with a bot-generated token, meaning `GITHUB_TOKEN` or a GitHub App installation token. With a PAT, `sign-commits` and its equivalents are a silent no-op: the action reports success, the commit is not signed, and the push is rejected.

Two limits of `createCommitOnBranch` to plan for. Its `FileAddition` input takes only `path` and `contents`, so it cannot set a file's executable bit. And it sends `expectedHeadOid`, so the commit is rejected if the branch moved since the run read the head. Re-read the head and retry rather than forcing.

##### External contributors

In public repos, Actions may run against PRs written by external contributors. These PRs should be reviewed thoroughly before approving workflows to run against them. Otherwise, a malicious PR could gain access to and steal all of the secrets available to the repo.

### Managing secrets

#### AWS

Application secrets are stored in AWS Secrets Manager. To modify an app's secrets, use our secrets tool.

#### GitHub

Secrets used by GitHub Actions are stored in GitHub secrets. All secrets should be stored in our GitHub org rather than in an individual repo. This allows us to more easily reuse secrets across repos, and also provides a holistic view of all of our secrets. The org secret should be scoped to the specific repos that need it.

### Reporting a security issue

If you believe we've been hit by a security issue, raise an incident. In the best case, it'll mean security folks look at it ASAP. In the worst case, it's a false positive and we can close the incident.

Canonical URL: https://posthog.com/handbook/engineering/security

GitHub source: contents/handbook/engineering/security.md

Content hash: `f075f3d1dcbbf91d`

Static reader notes

- `MDX_COMPONENT_STATIC_ADAPTER`: Adapted interactive MDX components for static reading: PrivateLink.