/
wax_boy
/
semaphore_custom
Обзор
Документация
Войти
/
wax_boy
/
semaphore_custom
Код
Запросы
0
Задачи
Вики
Пакеты
0
Релизы
0
CI/CD
Аналитика
Безопасность
develop
pkg/ssh/agent.go
236 строк
6 KB
Denis Gukov
feat(ssh): config for script host key checking
19 июл 2026, 17:13
19 июл 2026, 17:13
928f36a
Код
Авторство
О чём код?
package ssh import ( "fmt" "io" "net" "os" "path" "github.com/semaphoreui/semaphore/db" "github.com/semaphoreui/semaphore/pkg/random" "github.com/semaphoreui/semaphore/util" "github.com/semaphoreui/semaphore/pkg/task_logger" "golang.org/x/crypto/ssh" "golang.org/x/crypto/ssh/agent" ) type AgentKey struct { Key []byte Passphrase []byte } type Agent struct { Keys []AgentKey Logger task_logger.Logger listener net.Listener SocketFile string done chan struct{} } func NewAgent() Agent { return Agent{} } func (a *Agent) Listen() error { keyring := agent.NewKeyring() for _, k := range a.Keys { var ( key any err error ) if len(k.Passphrase) == 0 { key, err = ssh.ParseRawPrivateKey(k.Key) } else { key, err = ssh.ParseRawPrivateKeyWithPassphrase(k.Key, k.Passphrase) } if err != nil { return fmt.Errorf("parsing private key: %w", err) } if err := keyring.Add(agent.AddedKey{ PrivateKey: key, }); err != nil { return fmt.Errorf("adding private key: %w", err) } } if err := os.MkdirAll(path.Dir(a.SocketFile), 0o755); err != nil { return fmt.Errorf("creating socket directory: %w", err) } l, err := net.ListenUnix( "unix", &net.UnixAddr{ Net: "unix", Name: a.SocketFile, }, ) if err != nil { return fmt.Errorf("listening on socket %q: %w", a.SocketFile, err) } l.SetUnlinkOnClose(true) a.listener = l a.done = make(chan struct{}) go func() { for { conn, err := a.listener.Accept() if err != nil { select { case <-a.done: return default: a.Logger.Logf("error accepting socket connection: %w", err) return } } go func(conn net.Conn) { defer conn.Close() //nolint:errcheck if err := agent.ServeAgent(keyring, conn); err != nil && err != io.EOF { a.Logger.Logf("error serving SSH agent listener: %w", err) } }(conn) } }() return nil } func (a *Agent) Close() error { if a.done != nil { close(a.done) } if a.listener != nil { return a.listener.Close() } return nil } func StartSSHAgent(key db.AccessKey, logger task_logger.Logger) (Agent, error) { socketFilename := fmt.Sprintf("ssh-agent-%d-%s.sock", key.ID, random.String(10)) var socketFile string if key.ProjectID == nil { socketFile = path.Join(util.Config.TmpPath, socketFilename) } else { socketFile = path.Join(util.Config.GetProjectTmpDir(*key.ProjectID), socketFilename) } sshAgent := Agent{ Logger: logger, Keys: []AgentKey{ { Key: []byte(key.SshKey.PrivateKey), Passphrase: []byte(key.SshKey.Passphrase), }, }, SocketFile: socketFile, } return sshAgent, sshAgent.Listen() } type AccessKeyInstallation struct { SSHAgent *Agent Login string Password string Script string } func (key *AccessKeyInstallation) GetGitEnv() (env []string) { env = make([]string, 0) env = append(env, "GIT_TERMINAL_PROMPT=0") if key.SSHAgent != nil { env = append(env, fmt.Sprintf("SSH_AUTH_SOCK=%s", key.SSHAgent.SocketFile)) sshCmd := "ssh " + gitHostKeyCheckingOpts() if util.Config.GetSshConfigPath() != "" { sshCmd += " -F " + util.Config.GetSshConfigPath() } env = append(env, fmt.Sprintf("GIT_SSH_COMMAND=%s", sshCmd)) } return env } // gitHostKeyCheckingOpts returns the ssh host-key verification options used for // git operations. Host-key checking is enabled so a network attacker cannot // impersonate the git server. When an explicit known_hosts file is configured // it is used with strict checking; otherwise a persistent trust-on-first-use // file under TmpPath is used (accept-new): the first host key seen is pinned and // any subsequent change is rejected. func gitHostKeyCheckingOpts() string { switch util.Config.Ssh.StrictHostKeyChecking { case util.SshStrictHostKeyCheckingYes: return fmt.Sprintf("-o StrictHostKeyChecking=yes -o UserKnownHostsFile=%s", util.Config.Ssh.KnownHostsFile) case util.SshStrictHostKeyCheckingNo: return "ssh -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null" case util.SshStrictHostKeyCheckingAcceptNew: return fmt.Sprintf("-o StrictHostKeyChecking=accept-new -o UserKnownHostsFile=%s", util.Config.Ssh.KnownHostsFile) default: panic("Unknown SSH strict host key check option") } } func (key *AccessKeyInstallation) Destroy() error { if key.SSHAgent != nil { return key.SSHAgent.Close() } return nil } type KeyInstaller struct{} func (KeyInstaller) Install(key db.AccessKey, usage db.AccessKeyRole, logger task_logger.Logger) (installation AccessKeyInstallation, err error) { switch usage { case db.AccessKeyRoleGit: switch key.Type { case db.AccessKeySSH: var agent Agent agent, err = StartSSHAgent(key, logger) installation.SSHAgent = &agent installation.Login = key.SshKey.Login } case db.AccessKeyRoleAnsiblePasswordVault: switch key.Type { case db.AccessKeyLoginPassword: installation.Password = key.LoginPassword.Password default: err = fmt.Errorf("access key type not supported for ansible password vault") } case db.AccessKeyRoleAnsibleBecomeUser: if key.Type != db.AccessKeyLoginPassword { err = fmt.Errorf("access key type not supported for ansible become user") } installation.Login = key.LoginPassword.Login installation.Password = key.LoginPassword.Password case db.AccessKeyRoleAnsibleUser: switch key.Type { case db.AccessKeySSH: var agent Agent agent, err = StartSSHAgent(key, logger) installation.SSHAgent = &agent installation.Login = key.SshKey.Login case db.AccessKeyLoginPassword: installation.Login = key.LoginPassword.Login installation.Password = key.LoginPassword.Password case db.AccessKeyNone: // No SSH agent or password needed for ansible user with no access key. default: err = fmt.Errorf("access key type not supported for ansible user") } } return }