diff --git a/frontend/src/App.vue b/frontend/src/App.vue index ce21284..a1d3e4b 100644 --- a/frontend/src/App.vue +++ b/frontend/src/App.vue @@ -46,8 +46,8 @@ const activeTab = ref('general') // a one-line change. The label values match the user-facing tab names. const sidebarItems: Array<{ id: SettingsTab; label: string }> = [ { id: 'general', label: 'General' }, - { id: 's3', label: 'S3-Conf' }, - { id: 'ssh', label: 'SSH-Conf' }, + { id: 's3', label: 'S3' }, + { id: 'ssh', label: 'SSH' }, ] interface OverlayPayload { diff --git a/frontend/src/views/SettingsView.vue b/frontend/src/views/SettingsView.vue index fe3fdb2..4bcd2ca 100644 --- a/frontend/src/views/SettingsView.vue +++ b/frontend/src/views/SettingsView.vue @@ -78,7 +78,7 @@ function defaultConfig(): AppConfig { host: '', port: 22, user: '', - authMethod: 'builtin', + authMethod: 'password', password: '', pathPrefix: 'snapgo/', strictHostKey: false, @@ -286,7 +286,8 @@ onMounted(load) @@ -295,10 +296,10 @@ onMounted(load) @@ -340,16 +341,21 @@ onMounted(load)

Kerberos mode delegates to the system ssh/scp. - Run kinit {{ config.ssh.user || 'user' }}@BYTEDANCE.COM in a terminal first; tickets + Run kinit your.name@BYTEDANCE.COM in a terminal first; tickets expire (typically every 10–24h), so re-run kinit if uploads start failing. Verify with klist.

+

+ SSH-Key mode uses your ssh-agent and ~/.ssh/id_* + keys. Make sure password-less login already works (e.g. via + ssh-copy-id). +

- Password is empty — please make sure password-less SSH is configured on - this machine (e.g. via ssh-copy-id or your ssh-agent). + Password is empty — enter the remote login password, or switch the auth + method to SSH-Key.

diff --git a/internal/domain/config.go b/internal/domain/config.go index 237b44f..c6ed8ae 100644 --- a/internal/domain/config.go +++ b/internal/domain/config.go @@ -34,13 +34,17 @@ type S3Config struct { // - Host / Port form the destination endpoint. Port defaults to 22 when 0. // - User is the SSH login name. // - AuthMethod selects how the connection authenticates: -// "" / "builtin" → password / ssh-agent / ~/.ssh/id_* via the -// in-process golang.org/x/crypto/ssh client. +// "password" → password only, via the in-process Go SSH client. +// "key" → ssh-agent + ~/.ssh/id_* key files (no password), +// via the in-process Go SSH client. // "kerberos" → delegate to the system /usr/bin/ssh binary so the // existing Kerberos (GSSAPI) credential cache from // `kinit` is reused. Native GSSAPI is required here // because macOS stores tickets in an API: ccache // that pure-Go SSH libraries cannot read. +// "" / "builtin" → legacy combined behaviour (password → agent → key +// files) kept only for backward compatibility with +// configs written before the method was split. // - Password is optional; when empty the implementation falls back to the // local SSH agent or ~/.ssh/id_* keys (i.e. the user is responsible for // having password-less access already configured on this machine). @@ -70,9 +74,14 @@ type SSHConfig struct { // SSH authentication method identifiers stored in SSHConfig.AuthMethod. const ( - // SSHAuthBuiltin uses the in-process Go SSH client (password / agent / - // key files). This is the zero-value default. + // SSHAuthBuiltin is the legacy combined method (password → agent → key + // files). Retained for backward compatibility with older config files; + // new configs use the explicit methods below. SSHAuthBuiltin = "builtin" + // SSHAuthPassword authenticates with the password field only. + SSHAuthPassword = "password" + // SSHAuthKey authenticates with ssh-agent / ~/.ssh/id_* key files only. + SSHAuthKey = "key" // SSHAuthKerberos delegates to the system ssh binary so an existing // Kerberos credential cache (populated by `kinit`) is reused via GSSAPI. SSHAuthKerberos = "kerberos" diff --git a/internal/infrastructure/ssh/client.go b/internal/infrastructure/ssh/client.go index ff2ec9e..42c1364 100644 --- a/internal/infrastructure/ssh/client.go +++ b/internal/infrastructure/ssh/client.go @@ -385,7 +385,14 @@ func shellQuote(s string) string { } // buildAuthMethods chooses authentication methods given the supplied -// configuration. See Dial's docstring for the priority order. +// configuration. +// +// The method set is gated by cfg.AuthMethod: +// - SSHAuthPassword → password only. +// - SSHAuthKey → ssh-agent + ~/.ssh/id_* key files only. +// - "" / SSHAuthBuiltin (legacy) → password (if set) + agent + key files. +// +// (Kerberos never reaches here; it uses the system ssh binary instead.) // // Returns a human-readable summary alongside the method slice so the // caller can include "password+agent+ed25519" (and similar) in its dial @@ -393,47 +400,58 @@ func shellQuote(s string) string { func buildAuthMethods(cfg domain.SSHConfig) ([]ssh.AuthMethod, string, error) { var methods []ssh.AuthMethod var sources []string - if cfg.Password != "" { + + // Decide which families are allowed for the selected method. An empty / + // "builtin" value preserves the original combined behaviour so configs + // written before the split keep working. + allowPassword := cfg.AuthMethod == domain.SSHAuthPassword || + cfg.AuthMethod == domain.SSHAuthBuiltin || cfg.AuthMethod == "" + allowKey := cfg.AuthMethod == domain.SSHAuthKey || + cfg.AuthMethod == domain.SSHAuthBuiltin || cfg.AuthMethod == "" + + if allowPassword && cfg.Password != "" { methods = append(methods, ssh.Password(cfg.Password)) sources = append(sources, "password") } - if sock := os.Getenv("SSH_AUTH_SOCK"); sock != "" { - if conn, err := net.Dial("unix", sock); err == nil { - // 关键: 仅在 agent 真正持有 key 时才把它加入 auth methods. - // - // macOS 的 launchd ssh-agent 即便没有任何 identity 也会响应, - // 此时若仍注册 PublicKeysCallback, x/crypto 会把它当作一次空的 - // publickey 尝试. 配合服务器的 MaxAuthTries 计数, 这次"空尝试" - // 会挤占后续基于磁盘私钥的认证机会, 最终导致 - // "[none publickey] no supported methods remain" —— 即便磁盘上 - // 的 key 本身完全可用. 因此空 agent 必须跳过. - ag := agent.NewClient(conn) - if keys, lerr := ag.List(); lerr == nil && len(keys) > 0 { - methods = append(methods, ssh.PublicKeysCallback(ag.Signers)) - sources = append(sources, fmt.Sprintf("agent(%d)", len(keys))) + if allowKey { + if sock := os.Getenv("SSH_AUTH_SOCK"); sock != "" { + if conn, err := net.Dial("unix", sock); err == nil { + // 关键: 仅在 agent 真正持有 key 时才把它加入 auth methods. + // + // macOS 的 launchd ssh-agent 即便没有任何 identity 也会响应, + // 此时若仍注册 PublicKeysCallback, x/crypto 会把它当作一次空的 + // publickey 尝试. 配合服务器的 MaxAuthTries 计数, 这次"空尝试" + // 会挤占后续基于磁盘私钥的认证机会, 最终导致 + // "[none publickey] no supported methods remain" —— 即便磁盘上 + // 的 key 本身完全可用. 因此空 agent 必须跳过. + ag := agent.NewClient(conn) + if keys, lerr := ag.List(); lerr == nil && len(keys) > 0 { + methods = append(methods, ssh.PublicKeysCallback(ag.Signers)) + sources = append(sources, fmt.Sprintf("agent(%d)", len(keys))) + } else { + sshLog().Debug("ssh-agent has no identities; skipping", + "sock", sock, "list_err", lerr) + } } else { - sshLog().Debug("ssh-agent has no identities; skipping", - "sock", sock, "list_err", lerr) + sshLog().Debug("ssh-agent dial failed", "sock", sock, "err", err) + } + } + home, err := os.UserHomeDir() + if err == nil { + // Probe the common default keys; any unreadable / missing file is + // silently skipped so the user never sees noise about keys they did + // not set up. + for _, name := range []string{"id_ed25519", "id_rsa", "id_ecdsa"} { + signer, err := loadPrivateKey(path.Join(home, ".ssh", name)) + if err == nil && signer != nil { + methods = append(methods, ssh.PublicKeys(signer)) + sources = append(sources, name) + } } } else { - sshLog().Debug("ssh-agent dial failed", "sock", sock, "err", err) + sshLog().Debug("home dir unavailable for ssh keys", "err", err) } } - home, err := os.UserHomeDir() - if err == nil { - // Probe the common default keys; any unreadable / missing file is - // silently skipped so the user never sees noise about keys they did - // not set up. - for _, name := range []string{"id_ed25519", "id_rsa", "id_ecdsa"} { - signer, err := loadPrivateKey(path.Join(home, ".ssh", name)) - if err == nil && signer != nil { - methods = append(methods, ssh.PublicKeys(signer)) - sources = append(sources, name) - } - } - } else { - sshLog().Debug("home dir unavailable for ssh keys", "err", err) - } if len(sources) == 0 { return methods, "none", nil }