mirror of
https://github.com/go-gitea/gitea.git
synced 2026-06-22 01:27:16 +02:00
reject non auth keys
This commit is contained in:
parent
b57a6507e3
commit
eb87aa3ab8
@ -61,20 +61,26 @@ type Command struct {
|
||||
// operations (migration / mirror with a generated keypair). ssh runs
|
||||
// non-interactively (BatchMode) so the worker never hangs on an unknown host,
|
||||
// and the configured host-key policy is applied.
|
||||
func managedSSHCommand() string {
|
||||
func managedSSHCommand(identityFile string) string {
|
||||
var cmd string
|
||||
mode := setting.Migrations.SSHHostKeyChecking
|
||||
if mode == "no" {
|
||||
return "ssh -o BatchMode=yes -o StrictHostKeyChecking=no -o UserKnownHostsFile=" + util.ShellEscape(os.DevNull)
|
||||
}
|
||||
cmd := "ssh -o BatchMode=yes -o StrictHostKeyChecking=" + mode
|
||||
// Persist accepted host keys in a Gitea-managed file so a later key change
|
||||
// is detected (TOFU); fall back to ssh's default known_hosts if unset.
|
||||
if setting.AppDataPath != "" {
|
||||
knownHosts := filepath.Join(setting.AppDataPath, "home", ".ssh", "known_hosts")
|
||||
if err := os.MkdirAll(filepath.Dir(knownHosts), 0o700); err == nil {
|
||||
cmd += " -o UserKnownHostsFile=" + util.ShellEscape(knownHosts)
|
||||
cmd = "ssh -o BatchMode=yes -o StrictHostKeyChecking=no -o UserKnownHostsFile=" + util.ShellEscape(os.DevNull)
|
||||
} else {
|
||||
cmd = "ssh -o BatchMode=yes -o StrictHostKeyChecking=" + mode
|
||||
// Persist accepted host keys in a Gitea-managed file so a later key change
|
||||
// is detected (TOFU); fall back to ssh's default known_hosts if unset.
|
||||
if setting.AppDataPath != "" {
|
||||
knownHosts := filepath.Join(setting.AppDataPath, "home", ".ssh", "known_hosts")
|
||||
if err := os.MkdirAll(filepath.Dir(knownHosts), 0o700); err == nil {
|
||||
cmd += " -o UserKnownHostsFile=" + util.ShellEscape(knownHosts)
|
||||
}
|
||||
}
|
||||
}
|
||||
// pin auth to the managed key so ssh ignores the OS user's $HOME/.ssh identities
|
||||
if identityFile != "" {
|
||||
cmd += " -o IdentitiesOnly=yes -i " + util.ShellEscape(identityFile)
|
||||
}
|
||||
return cmd
|
||||
}
|
||||
|
||||
@ -242,6 +248,9 @@ type runOpts struct {
|
||||
// If provided, SSH_AUTH_SOCK environment variable will be set
|
||||
SSHAuthSock string
|
||||
|
||||
// SSHIdentityFile is the managed public key file used to pin ssh authentication
|
||||
SSHIdentityFile string
|
||||
|
||||
PipelineFunc func(Context) error
|
||||
}
|
||||
|
||||
@ -305,6 +314,11 @@ func (c *Command) WithSSHAuthSock(sshAuthSock string) *Command {
|
||||
return c
|
||||
}
|
||||
|
||||
func (c *Command) WithSSHIdentityFile(identityFile string) *Command {
|
||||
c.opts.SSHIdentityFile = identityFile
|
||||
return c
|
||||
}
|
||||
|
||||
func (c *Command) makeStdoutStderr(w *io.Writer) (PipeReader, func()) {
|
||||
pr, pw, err := os.Pipe()
|
||||
if err != nil {
|
||||
@ -475,7 +489,7 @@ func (c *Command) Start(ctx context.Context) (retErr error) {
|
||||
|
||||
if c.opts.SSHAuthSock != "" {
|
||||
c.cmd.Env = append(c.cmd.Env, "SSH_AUTH_SOCK="+c.opts.SSHAuthSock)
|
||||
c.cmd.Env = append(c.cmd.Env, "GIT_SSH_COMMAND="+managedSSHCommand())
|
||||
c.cmd.Env = append(c.cmd.Env, "GIT_SSH_COMMAND="+managedSSHCommand(c.opts.SSHIdentityFile))
|
||||
}
|
||||
|
||||
c.cmd.Dir = c.opts.Dir
|
||||
|
||||
@ -30,7 +30,7 @@ func TestManagedSSHCommand(t *testing.T) {
|
||||
|
||||
knownHosts := filepath.Join(dataPath, "home", ".ssh", "known_hosts")
|
||||
expected := "ssh -o BatchMode=yes -o StrictHostKeyChecking=accept-new -o UserKnownHostsFile=" + util.ShellEscape(knownHosts)
|
||||
assert.Equal(t, expected, managedSSHCommand())
|
||||
assert.Equal(t, expected, managedSSHCommand(""))
|
||||
|
||||
// the known_hosts directory must be created so ssh can write to it
|
||||
info, err := os.Stat(filepath.Dir(knownHosts))
|
||||
@ -45,7 +45,7 @@ func TestManagedSSHCommand(t *testing.T) {
|
||||
|
||||
knownHosts := filepath.Join(dataPath, "home", ".ssh", "known_hosts")
|
||||
expected := "ssh -o BatchMode=yes -o StrictHostKeyChecking=yes -o UserKnownHostsFile=" + util.ShellEscape(knownHosts)
|
||||
assert.Equal(t, expected, managedSSHCommand())
|
||||
assert.Equal(t, expected, managedSSHCommand(""))
|
||||
})
|
||||
|
||||
t.Run("no disables verification with /dev/null known_hosts", func(t *testing.T) {
|
||||
@ -53,21 +53,28 @@ func TestManagedSSHCommand(t *testing.T) {
|
||||
setting.Migrations.SSHHostKeyChecking = "no"
|
||||
|
||||
expected := "ssh -o BatchMode=yes -o StrictHostKeyChecking=no -o UserKnownHostsFile=" + util.ShellEscape(os.DevNull)
|
||||
assert.Equal(t, expected, managedSSHCommand())
|
||||
assert.Equal(t, expected, managedSSHCommand(""))
|
||||
})
|
||||
|
||||
t.Run("empty AppDataPath falls back without UserKnownHostsFile", func(t *testing.T) {
|
||||
setting.AppDataPath = ""
|
||||
setting.Migrations.SSHHostKeyChecking = "accept-new"
|
||||
|
||||
assert.Equal(t, "ssh -o BatchMode=yes -o StrictHostKeyChecking=accept-new", managedSSHCommand())
|
||||
assert.Equal(t, "ssh -o BatchMode=yes -o StrictHostKeyChecking=accept-new", managedSSHCommand(""))
|
||||
})
|
||||
|
||||
t.Run("BatchMode is always set so the worker never hangs", func(t *testing.T) {
|
||||
setting.AppDataPath = t.TempDir()
|
||||
for _, mode := range []string{"accept-new", "yes", "no"} {
|
||||
setting.Migrations.SSHHostKeyChecking = mode
|
||||
assert.Contains(t, managedSSHCommand(), "ssh -o BatchMode=yes ", "mode %q must run ssh non-interactively", mode)
|
||||
assert.Contains(t, managedSSHCommand(""), "ssh -o BatchMode=yes ", "mode %q must run ssh non-interactively", mode)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("identityFile pins authentication to the managed key", func(t *testing.T) {
|
||||
setting.AppDataPath = t.TempDir()
|
||||
setting.Migrations.SSHHostKeyChecking = "no"
|
||||
keyFile := filepath.Join(t.TempDir(), "id.pub")
|
||||
assert.Contains(t, managedSSHCommand(keyFile), "-o IdentitiesOnly=yes -i "+util.ShellEscape(keyFile))
|
||||
})
|
||||
}
|
||||
|
||||
@ -99,19 +99,20 @@ func (repo *Repository) IsEmpty() (bool, error) {
|
||||
|
||||
// CloneRepoOptions options when clone a repository
|
||||
type CloneRepoOptions struct {
|
||||
Timeout time.Duration
|
||||
Mirror bool
|
||||
Bare bool
|
||||
Quiet bool
|
||||
Branch string
|
||||
Shared bool
|
||||
NoCheckout bool
|
||||
Depth int
|
||||
Filter string
|
||||
SkipTLSVerify bool
|
||||
SSHAuthSock string
|
||||
SingleBranch bool
|
||||
Env []string
|
||||
Timeout time.Duration
|
||||
Mirror bool
|
||||
Bare bool
|
||||
Quiet bool
|
||||
Branch string
|
||||
Shared bool
|
||||
NoCheckout bool
|
||||
Depth int
|
||||
Filter string
|
||||
SkipTLSVerify bool
|
||||
SSHAuthSock string
|
||||
SSHIdentityFile string
|
||||
SingleBranch bool
|
||||
Env []string
|
||||
}
|
||||
|
||||
// Clone clones original repository to target path.
|
||||
@ -172,20 +173,22 @@ func Clone(ctx context.Context, from, to string, opts CloneRepoOptions) error {
|
||||
WithTimeout(opts.Timeout).
|
||||
WithEnv(envs).
|
||||
WithSSHAuthSock(opts.SSHAuthSock).
|
||||
WithSSHIdentityFile(opts.SSHIdentityFile).
|
||||
RunWithStderr(ctx)
|
||||
}
|
||||
|
||||
// PushOptions options when push to remote
|
||||
type PushOptions struct {
|
||||
Remote string
|
||||
LocalRefName string
|
||||
Branch string
|
||||
Force bool
|
||||
ForceWithLease string
|
||||
Mirror bool
|
||||
Env []string
|
||||
Timeout time.Duration
|
||||
SSHAuthSock string
|
||||
Remote string
|
||||
LocalRefName string
|
||||
Branch string
|
||||
Force bool
|
||||
ForceWithLease string
|
||||
Mirror bool
|
||||
Env []string
|
||||
Timeout time.Duration
|
||||
SSHAuthSock string
|
||||
SSHIdentityFile string
|
||||
}
|
||||
|
||||
// Push pushs local commits to given remote branch.
|
||||
@ -211,7 +214,7 @@ func Push(ctx context.Context, repoPath string, opts PushOptions) error {
|
||||
}
|
||||
cmd.AddDashesAndList(remoteBranchArgs...)
|
||||
|
||||
stdout, stderr, err := cmd.WithEnv(opts.Env).WithTimeout(opts.Timeout).WithDir(repoPath).WithSSHAuthSock(opts.SSHAuthSock).RunStdString(ctx)
|
||||
stdout, stderr, err := cmd.WithEnv(opts.Env).WithTimeout(opts.Timeout).WithDir(repoPath).WithSSHAuthSock(opts.SSHAuthSock).WithSSHIdentityFile(opts.SSHIdentityFile).RunStdString(ctx)
|
||||
if err != nil {
|
||||
if strings.Contains(stderr, "non-fast-forward") {
|
||||
return &ErrPushOutOfDate{StdOut: stdout, StdErr: stderr, Err: err}
|
||||
|
||||
@ -7,6 +7,7 @@ import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
|
||||
repo_model "gitea.dev/models/repo"
|
||||
user_model "gitea.dev/models/user"
|
||||
@ -47,10 +48,10 @@ func GetSSHKeypairForRepository(ctx context.Context, repo *repo_model.Repository
|
||||
// If sshKeyOwnerID is non-zero, the keypair of that owner is used instead of
|
||||
// the repository owner's (used when migrating to an org and the user wants
|
||||
// to authenticate with their personal managed key).
|
||||
func SetupManagedSSHAgent(ctx context.Context, repo *repo_model.Repository, remoteURL string, sshKeyOwnerID int64) (sshAuthSock string, cleanup func(), err error) {
|
||||
func SetupManagedSSHAgent(ctx context.Context, repo *repo_model.Repository, remoteURL string, sshKeyOwnerID int64) (sshAuthSock, sshIdentityFile string, cleanup func(), err error) {
|
||||
noop := func() {}
|
||||
if !IsSSHURL(remoteURL) {
|
||||
return "", noop, nil
|
||||
return "", "", noop, nil
|
||||
}
|
||||
|
||||
ownerID := repo.OwnerID
|
||||
@ -59,22 +60,53 @@ func SetupManagedSSHAgent(ctx context.Context, repo *repo_model.Repository, remo
|
||||
}
|
||||
keypair, err := GetOrCreateSSHKeypair(ctx, ownerID)
|
||||
if err != nil {
|
||||
return "", noop, fmt.Errorf("failed to get SSH keypair for owner %d: %w", ownerID, err)
|
||||
return "", "", noop, fmt.Errorf("failed to get SSH keypair for owner %d: %w", ownerID, err)
|
||||
}
|
||||
if keypair == nil {
|
||||
return "", noop, nil
|
||||
return "", "", noop, nil
|
||||
}
|
||||
|
||||
privateKey, err := keypair.GetDecryptedPrivateKey()
|
||||
if err != nil {
|
||||
return "", noop, fmt.Errorf("failed to decrypt SSH private key: %w", err)
|
||||
return "", "", noop, fmt.Errorf("failed to decrypt SSH private key: %w", err)
|
||||
}
|
||||
|
||||
socketPath, agentCleanup, err := CreateTemporaryAgent(privateKey)
|
||||
if err != nil {
|
||||
return "", noop, fmt.Errorf("failed to create SSH agent: %w", err)
|
||||
return "", "", noop, fmt.Errorf("failed to create SSH agent: %w", err)
|
||||
}
|
||||
|
||||
identityFile, keyCleanup, err := writeManagedPublicKey(keypair.PublicKey)
|
||||
if err != nil {
|
||||
agentCleanup()
|
||||
return "", "", noop, fmt.Errorf("failed to write managed public key: %w", err)
|
||||
}
|
||||
|
||||
cleanup = func() {
|
||||
keyCleanup()
|
||||
agentCleanup()
|
||||
}
|
||||
|
||||
log.Debug("SSH agent ready for %s (socket: %s)", repo.FullName(), socketPath)
|
||||
return socketPath, agentCleanup, nil
|
||||
return socketPath, identityFile, cleanup, nil
|
||||
}
|
||||
|
||||
// writeManagedPublicKey writes the managed public key to a temporary file so the
|
||||
// git SSH command can pin authentication to it via "-i". The returned cleanup
|
||||
// removes the file.
|
||||
func writeManagedPublicKey(publicKey string) (path string, cleanup func(), err error) {
|
||||
f, err := os.CreateTemp("", "gitea-managed-ssh-*.pub")
|
||||
if err != nil {
|
||||
return "", nil, err
|
||||
}
|
||||
if _, err = f.WriteString(publicKey); err != nil {
|
||||
f.Close()
|
||||
os.Remove(f.Name())
|
||||
return "", nil, err
|
||||
}
|
||||
if err = f.Close(); err != nil {
|
||||
os.Remove(f.Name())
|
||||
return "", nil, err
|
||||
}
|
||||
return f.Name(), func() { os.Remove(f.Name()) }, nil
|
||||
}
|
||||
|
||||
@ -119,7 +119,7 @@ func runSync(ctx context.Context, m *repo_model.Mirror) ([]*repo_module.SyncResu
|
||||
timeout := time.Duration(setting.Git.Timeout.Mirror) * time.Second
|
||||
|
||||
// Setup SSH authentication if needed
|
||||
sshAuthSock, cleanup, sshErr := ssh_module.SetupManagedSSHAgent(ctx, m.Repo, remoteURL.String(), 0)
|
||||
sshAuthSock, sshIdentityFile, cleanup, sshErr := ssh_module.SetupManagedSSHAgent(ctx, m.Repo, remoteURL.String(), 0)
|
||||
if sshErr != nil {
|
||||
log.Error("SyncMirrors [repo: %-v]: SSH setup error %v", m.Repo, sshErr)
|
||||
return nil, false
|
||||
@ -132,7 +132,7 @@ func runSync(ctx context.Context, m *repo_model.Mirror) ([]*repo_module.SyncResu
|
||||
if m.EnablePrune {
|
||||
cmd.AddArguments("--prune")
|
||||
}
|
||||
return cmd.AddDynamicArguments(m.GetRemoteName()).WithTimeout(timeout).WithEnv(envs).WithSSHAuthSock(sshAuthSock)
|
||||
return cmd.AddDynamicArguments(m.GetRemoteName()).WithTimeout(timeout).WithEnv(envs).WithSSHAuthSock(sshAuthSock).WithSSHIdentityFile(sshIdentityFile)
|
||||
}
|
||||
|
||||
var err error
|
||||
@ -209,7 +209,7 @@ func runSync(ctx context.Context, m *repo_model.Mirror) ([]*repo_module.SyncResu
|
||||
|
||||
cmdRemoteUpdatePrune := func() *gitcmd.Command {
|
||||
return gitcmd.NewCommand("remote", "update", "--prune").
|
||||
AddDynamicArguments(m.GetRemoteName()).WithTimeout(timeout).WithEnv(envs).WithSSHAuthSock(sshAuthSock)
|
||||
AddDynamicArguments(m.GetRemoteName()).WithTimeout(timeout).WithEnv(envs).WithSSHAuthSock(sshAuthSock).WithSSHIdentityFile(sshIdentityFile)
|
||||
}
|
||||
|
||||
if repo_service.HasWiki(ctx, m.Repo) {
|
||||
|
||||
@ -168,13 +168,14 @@ func runPushSync(ctx context.Context, m *repo_model.PushMirror) error {
|
||||
}
|
||||
|
||||
// Setup SSH authentication
|
||||
sshAuthSock, cleanup, err := ssh_module.SetupManagedSSHAgent(ctx, repo, remoteURL.String(), 0)
|
||||
sshAuthSock, sshIdentityFile, cleanup, err := ssh_module.SetupManagedSSHAgent(ctx, repo, remoteURL.String(), 0)
|
||||
if err != nil {
|
||||
log.Error("Failed to set up SSH agent for push mirror %s: %v", repo.FullName(), err)
|
||||
return util.SanitizeErrorCredentialURLs(err)
|
||||
}
|
||||
defer cleanup()
|
||||
pushOpts.SSHAuthSock = sshAuthSock
|
||||
pushOpts.SSHIdentityFile = sshIdentityFile
|
||||
|
||||
if err := gitrepo.PushToExternal(ctx, storageRepo, pushOpts); err != nil {
|
||||
log.Error("Error pushing %s mirror[%d] remote %s: %v", storageRepo.RelativePath(), m.ID, m.RemoteName, err)
|
||||
|
||||
@ -29,13 +29,14 @@ import (
|
||||
)
|
||||
|
||||
func cloneExternalRepoWithSSHAuth(ctx context.Context, repo *repo_model.Repository, remoteURL string, storageRepo gitrepo.Repository, cloneOpts git.CloneRepoOptions, sshKeyOwnerID int64) error {
|
||||
sshAuthSock, cleanup, err := ssh_module.SetupManagedSSHAgent(ctx, repo, remoteURL, sshKeyOwnerID)
|
||||
sshAuthSock, sshIdentityFile, cleanup, err := ssh_module.SetupManagedSSHAgent(ctx, repo, remoteURL, sshKeyOwnerID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer cleanup()
|
||||
|
||||
cloneOpts.SSHAuthSock = sshAuthSock
|
||||
cloneOpts.SSHIdentityFile = sshIdentityFile
|
||||
return gitrepo.CloneExternalRepo(ctx, remoteURL, storageRepo, cloneOpts)
|
||||
}
|
||||
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user