Repository navigation
feat(git): private repositories for git stacks (#138, part 2) - #174
Merged
Merged
Conversation
GitCommand::run takes a list of extra settings, passed with -c after the forced ones. Private repositories need this: a stack's credential helper and, for ssh, allowing that transport, apply to that stack's runs only. The forced settings still come first, so the empty credential.helper resets the helper list before a stack's own helper is added. The extra settings are built by the plugin, never taken from user input, and never hold a secret, since a command line is visible to anyone on the server. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
git.json gets its first key added after format 1: credentialId, the credential vault entry used to reach the repository. Missing means none, which is what format 1 did, so every existing file keeps its meaning. save() writes it only when one is set. A stack without a credential then still loads in a version from before this change, and one with a credential is refused there by name, which is right: that version could not reach the repository anyway. It is a key of its own rather than the stack's credential_id file, which holds the registry credential for pulling images: a stack can need both. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The vault gets a "git" provider: an HTTPS token for reaching a git repository, such as a GitHub fine-grained token or a Forgejo access token. Its registry field holds the repository host only (github.com, or host:port), and saving anything else is refused. useCredential() hands one credential, secret included, to a callback. Git stacks need it to pass the token to git; the secret is never returned to the caller. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A git stack with a credential gets it on every git run: the token is written to a file only root can read under /var/tmp (in RAM), git is pointed at it with credential.helper=store, and the file is removed when the run ends. A file left by a run that was killed is removed by a later run once it is an hour old: every git run is time-limited to far less. The token is never on a command line, in the environment, in the clone's .git/config or in a log. The file holds the stack's own repository address, and git matches the path as well as the host (credential.useHttpPath), so the token is offered to that repository only. A credential for another host, or one that is not a git credential, is refused by name. Every run gets it, not only clone and fetch, because the clone is partial (--filter=blob:none): a checkout can fetch file contents too. Two git messages that point the wrong way get a line of their own: a server that redirects to another address (the token is not offered there), and a repository that asks for a password when the stack has no credential. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The credential list's "Used by" and the refusal to delete a credential that a stack uses looked only at each stack's credential_id file (its registry credential). They now also read the credentialId in a git stack's git.json. git.json is read as plain JSON, not through its full validation, so a file that has since become invalid still keeps its credential from being deleted. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
add and convert take --credential <name>, a git credential from the plugin's Credentials tab, for a private repository. The new credential command changes or removes it on an existing stack, and saves the change only after the repository has been reached with it. status shows which credential a stack uses. A credential is found by its exact name or its id, and only among git credentials; two with the same name are not guessed between. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…als tab The provider list gets "Git repository (HTTPS token, for git stacks)", with help on which host to enter and what token to use. Test on a git credential reaches the repository of a git stack that uses it, instead of the Docker registry handshake, which a git server does not answer. With no stack using it yet there is no repository to try, and it says so. A git token is left out of a stack's registry credential picker, and assigning one there is refused: it cannot be used to pull images. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A git stack's repository can now be an ssh address, ssh://git@host[:port]/path or git@host:path, reached with the stack's deploy key (its credentialId). The user, host and port go to ssh, so each is held to plain characters and none may start with "-", where ssh would read it as an option. git.json gets a second optional key, sshKnownHosts: the repository host's keys in known_hosts format, pinned when the stack is set up. Like credentialId it is written only when set, and it is refused on an https or local repository. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The vault gets a "git-ssh" provider: the private half of a git stack's ssh deploy key, kept encrypted like every other secret the plugin holds, for one repository host. Git stacks make these themselves; nothing in the Credentials tab creates one. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
An ssh stack's git runs get its deploy key, written from the vault to a root-only file for the run, and its pinned host keys, and ssh is told to use those and nothing else: no config file, only that key, no prompts, and strict host key checking against the pinned keys only. A host whose key has changed therefore stops the run. ssh is allowed as a transport for that stack's runs only. GitSsh also makes a new ed25519 key pair, reads a deploy key's public half back from the vault (so it is not stored twice), asks a host for its keys, and shows their SHA256 fingerprints for a person to compare. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…e-git add and convert with an ssh address make a deploy key for the stack (an ed25519 key in the vault, named "<stack> deploy key") and pin the host's keys, printing their fingerprints to compare with the ones the git host publishes. If the clone then fails, as it does until the repository knows the key, the public key is shown with what to do: add it as a read-only deploy key and run the same command again, which reuses the key made the first time. deploy-key shows a stack's public key again. trust-host pins the host's keys again after the server's keys change, showing the old and new fingerprints, and saves them only once the repository has been reached. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A deploy key made by compose-git is listed as "SSH deploy key (git stack)" with the stack that uses it. It has no Edit button, since it is a key pair, not something typed in. Test reaches the stack's repository with it, as for an HTTPS token, and it cannot be chosen or set as a stack's registry credential. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A new section on reaching a private repository: an HTTPS access token added on the Credentials tab and named with --credential, or an ssh deploy key that the stack gets of its own, with what to do the first time and when the server's host keys change. The command reference gets --credential, credential, deploy-key and trust-host, and the limitation about public repositories is replaced by the one that remains: an https server needs a certificate this server trusts. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
compose-git looked credentials up among HTTPS tokens and deploy keys alike, so --credential and compose-git credential could give a stack another stack's deploy key, against the rule that each ssh stack has a key of its own (and leaving its own key unused in the vault). Only HTTPS tokens are found by name or id now. Adding or converting an ssh stack with --credential is refused before anything is made, and an ssh stack's credential cannot be changed: it keeps the key made for it. Found by Copilot on mstrhakr#174. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Editing a credential could change it from a registry login to a git token or back, keeping its id. A stack using it then failed at its next run: a git stack needs a git credential, and a registry login is never a git one (and a hand-made request could even turn a deploy key into a token). The vault now refuses to change a saved credential between a registry kind and a git kind; changes among registry kinds stay allowed, as before. The Credentials tab's Edit no longer offers such a change, and the web action refuses to add a deploy key, which only compose-git makes. Found by Copilot on mstrhakr#174. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…nt lock Deleting a credential takes the assignment lock and refuses while a stack uses it. compose-git add, convert and credential reached the repository first, then saved the stack's credential without that lock. A delete in between (during a clone, say) was allowed, since no stack used the credential yet, and the stack was then saved naming a credential that no longer existed. A new ssh stack's deploy key could be lost this way. These three now check that the credential still exists and save, both under the lock, as the web UI's stack settings already do. The lock is not held while the repository is reached. The docs also say not to delete a new ssh stack's key before the second run of add. Found by Copilot on mstrhakr#174. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Git run files (an HTTPS token, a deploy key, pinned host keys) go into a folder in /var/tmp, where anyone can create things. A folder already there was used as it was: one another user made first, or a symlink, would let its owner read a token while its file was still readable, swap the pinned host keys, or have root clear out another folder. The folder is now used only when it is a plain folder owned by whoever runs the plugin (root on Unraid), and run files are created private rather than made private after. Found by Copilot on mstrhakr#174. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Author
|
Thanks Copilot, all four are right, and each is fixed as its own commit on top:
|
mstrhakr
pushed a commit
that referenced
this pull request
Oct 9, 2026
Git runs for git stacks use a home folder in /var/tmp, where anyone can create things, and the global config git reads (the list of trusted repositories) is kept there. A folder already there was used as it was: if another user made it first, or put a symlink there, they could give git a config of their own, which root then ran with. The folder is now used only when it is a plain folder owned by whoever runs the plugin (root on Unraid), and made private; otherwise git is not run and the result says why. Found while checking Copilot's findings on #174, which raised the same pattern for the folder that holds git credentials. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Second of four PRs for git-backed stacks (#138). Part 1 handles public repositories; this adds private ones,
in the two ways git hosts offer:
Credentials tab, given to a stack with
compose-git add ... --credential <name>.compose-git addwith an ssh address makes a key for that stack, keeps the private halfin the vault, prints the public half to add to the repository, and pins the server's host keys. One key per
stack, because GitHub allows a deploy key on one repository only.
How a secret reaches git: never through the command line, the environment,
.git/configor a log. For eachrun it is written to a root-only file in RAM and removed afterwards, the same pattern the vault already uses
for registry logins. A token is offered only for the stack's own repository; ssh uses only the stack's key and
pinned host keys, so a changed host key stops the run.
Also:
compose-git credential,deploy-keyandtrust-host; "Used by" and delete protection count gitstacks; a git credential can't be picked as a registry login;
git.jsongains two optional keys, so olderversions refuse such a stack by name rather than half-read it.
Reading it: 13 commits, in order, each one step with its tests: per-run git settings,
credentialId,the token kind, tokens reaching git, "Used by",
--credential, the Credentials tab; then the same for ssh(addresses and host keys, the key kind,
GitSsh,compose-gitsetup, the Credentials tab); then docs.Testing: 56 new PHPUnit tests; the full suite and PHPStan pass at every commit. On Unraid 7.3.2 against a
private repository on a Forgejo server, two end-to-end scripts (17 checks each) covered add, deploy and
redeploy with a token and with a deploy key, wrong and missing credentials refused with nothing left behind,
no secret in the clone, stack folder or syslog, and a changed host key stopping a deploy until
trust-host.