GitHub Credential Security#
Overview#
Dokploy stores GitHub App credentials (private key, client secret, webhook secret) in the database for server-side deploy and API operations. A project-wide convention ensures these secrets are never returned to API clients β every read path serving UI or external consumers strips them at the query or serialization layer. Server-side code that needs real credentials re-fetches them directly via dedicated lookup functions.
Two vulnerabilities shaped this convention:
- GHSA-hg9j-j5mc-phf5 / GHSA-wx75-vxph-2m2f β
application.onewas disclosing git-provider secrets to users with onlyservice:read(fixed in PR #4859) - Credential leak via
gitProvider.getAllβ all secret columns were returned in the list response (fixed in PR #4569)
Secret Redaction Patterns#
1. findApplicationById β Query-level exclusion#
findApplicationById in packages/server/src/services/application.ts excludes secret columns from nested provider relations directly in the Drizzle ORM query using columns: { ...: false }:
- GitHub:
githubClientSecret,githubPrivateKey,githubWebhookSecret - GitLab:
secret,accessToken,refreshToken - Bitbucket:
appPassword,apiToken - Gitea:
clientSecret,accessToken,refreshToken - Registry passwords use the same pattern:
registry,buildRegistry,rollbackRegistryall excludepassword
Drizzle types the columns option against each table's column list, so a misspelled column name fails tsc β the exclusions are validated at compile time .
2. gitProvider.getAll β Response-level allowlisting#
The getAll tRPC procedure fetches full provider objects (credentials are needed to compute derived booleans), then explicitly reconstructs each provider with only safe fields before serializing the response :
- GitHub:
githubId,githubAppName,githubAppId,githubInstallationId, plusisConfigured(computed from presence of private key / app ID / installation ID) - GitLab:
gitlabId,applicationId,gitlabUrl, plusisConfigured - Bitbucket:
bitbucketId,bitbucketUsername,isConfigured,isDeprecated - Gitea:
giteaId,giteaUrl,clientId, plusisConfigured
The UI consumes these derived isConfigured/isDeprecated flags instead of checking raw credential values directly.
Unredacted Credentials for Deploy & API Operations#
The convention: server-side code that needs real credentials never reads from findApplicationById's result. It calls the dedicated provider lookup functions β findGithubById, findGitlabById, findGiteaById, findBitbucketById β which return unredacted rows directly from the github/gitlab/etc. tables.
Three key flows rely on this:
Repository cloning β cloneGithubRepository calls findGithubById to get the private key, authenticates via authGithub, exchanges for an installation token via getGithubToken, and injects it into the clone URL as https://oauth2:<token>@github.com/<owner>/<repo>.git . The token is short-lived (GitHub installation tokens expire after 1 hour) and never logged.
Webhook signature verification β The GitHub webhook endpoint /api/deploy/github fetches the provider record by installationId to verify the HMAC-SHA-256 signature against githubWebhookSecret. This is the only read path for githubWebhookSecret, and it reads directly from the database .
Preview deployment PR comments β Functions that post or update Octokit PR comments (issueCommentExists, updateIssueComment, createPreviewDeploymentComment, createSecurityBlockedComment) each call findGithubById independently to obtain credentials at call time, rather than depending on a previously-fetched application object.
Note on
createPreviewDeployment: This function callsauthGithub(application?.github as Github)whereapplication.githubcomes fromfindApplicationByIdβ butgithubPrivateKeyis excluded in that query. This is a latent inconsistency:authGithubwill throwNOT_FOUNDat runtime if private key is absent. PR comment operations in the preview flow work around this by re-fetching viafindGithubByIddirectly.
Collaborator Permission Gate for Preview Deployments#
To prevent arbitrary PR authors from triggering preview deployments (which execute code and can access server secrets), the webhook handler enforces a per-application permission check :
- When
previewRequireCollaboratorPermissions !== false(the default),checkUserRepositoryPermissionsqueriesoctokit.rest.repos.getCollaboratorPermissionLevel. Onlywrite,maintain, oradminpermission levels pass . - Blocked authors receive a security notification comment via
createSecurityBlockedComment, with deduplication viahasExistingSecurityCommentto avoid repeat comments on subsequent pushes. - On API error (e.g., 404 from GitHub for non-collaborators), the function returns
hasWriteAccess: falserather than throwing, so errors deny-safe .
Key Files#
| File | Role |
|---|---|
packages/server/src/services/application.ts | findApplicationById β query-level secret exclusion |
apps/dokploy/server/api/routers/git-provider.ts | getAll β response-level allowlisting |
packages/server/src/services/github.ts | findGithubById, PR comment helpers, security blocked message |
packages/server/src/utils/providers/github.ts | authGithub, getGithubToken, cloneGithubRepository, checkUserRepositoryPermissions |
apps/dokploy/pages/api/deploy/github.ts | Webhook handler β signature verification, permission gate, deploy fan-out |
packages/server/src/services/preview-deployment.ts | createPreviewDeployment β preview lifecycle |