diff --git a/internal/certacme/certifiers/sp_ssh.go b/internal/certacme/certifiers/sp_ssh.go
index d4c9c9465..16ad0646f 100644
--- a/internal/certacme/certifiers/sp_ssh.go
+++ b/internal/certacme/certifiers/sp_ssh.go
@@ -19,25 +19,27 @@ func init() {
jumpServers := make([]chlgimpl.ServerConfig, len(credentials.JumpServers))
for i, jumpServer := range credentials.JumpServers {
jumpServers[i] = chlgimpl.ServerConfig{
- SshHost: jumpServer.Host,
- SshPort: jumpServer.Port,
- SshAuthMethod: jumpServer.AuthMethod,
- SshUsername: jumpServer.Username,
- SshPassword: jumpServer.Password,
- SshKey: jumpServer.Key,
- SshKeyPassphrase: jumpServer.KeyPassphrase,
+ SshHost: jumpServer.Host,
+ SshPort: jumpServer.Port,
+ SshAuthMethod: jumpServer.AuthMethod,
+ SshUsername: jumpServer.Username,
+ SshPassword: jumpServer.Password,
+ SshKey: jumpServer.Key,
+ SshKeyPassphrase: jumpServer.KeyPassphrase,
+ SshKeyCertificate: jumpServer.KeyCertificate,
}
}
provider, err := chlgimpl.NewChallenger(&chlgimpl.ChallengerConfig{
ServerConfig: chlgimpl.ServerConfig{
- SshHost: credentials.Host,
- SshPort: credentials.Port,
- SshAuthMethod: credentials.AuthMethod,
- SshUsername: credentials.Username,
- SshPassword: credentials.Password,
- SshKey: credentials.Key,
- SshKeyPassphrase: credentials.KeyPassphrase,
+ SshHost: credentials.Host,
+ SshPort: credentials.Port,
+ SshAuthMethod: credentials.AuthMethod,
+ SshUsername: credentials.Username,
+ SshPassword: credentials.Password,
+ SshKey: credentials.Key,
+ SshKeyPassphrase: credentials.KeyPassphrase,
+ SshKeyCertificate: credentials.KeyCertificate,
},
JumpServers: jumpServers,
UseSCP: xmaps.GetBool(options.ProviderExtendedConfig, "useSCP"),
diff --git a/internal/certmgmt/deployers/sp_ssh.go b/internal/certmgmt/deployers/sp_ssh.go
index 5a1e5d297..16d764ea1 100644
--- a/internal/certmgmt/deployers/sp_ssh.go
+++ b/internal/certmgmt/deployers/sp_ssh.go
@@ -19,25 +19,27 @@ func init() {
jumpServers := make([]dplyimpl.ServerConfig, len(credentials.JumpServers))
for i, jumpServer := range credentials.JumpServers {
jumpServers[i] = dplyimpl.ServerConfig{
- SshHost: jumpServer.Host,
- SshPort: jumpServer.Port,
- SshAuthMethod: jumpServer.AuthMethod,
- SshUsername: jumpServer.Username,
- SshPassword: jumpServer.Password,
- SshKey: jumpServer.Key,
- SshKeyPassphrase: jumpServer.KeyPassphrase,
+ SshHost: jumpServer.Host,
+ SshPort: jumpServer.Port,
+ SshAuthMethod: jumpServer.AuthMethod,
+ SshUsername: jumpServer.Username,
+ SshPassword: jumpServer.Password,
+ SshKey: jumpServer.Key,
+ SshKeyPassphrase: jumpServer.KeyPassphrase,
+ SshKeyCertificate: jumpServer.KeyCertificate,
}
}
provider, err := dplyimpl.NewDeployer(&dplyimpl.DeployerConfig{
ServerConfig: dplyimpl.ServerConfig{
- SshHost: credentials.Host,
- SshPort: credentials.Port,
- SshAuthMethod: credentials.AuthMethod,
- SshUsername: credentials.Username,
- SshPassword: credentials.Password,
- SshKey: credentials.Key,
- SshKeyPassphrase: credentials.KeyPassphrase,
+ SshHost: credentials.Host,
+ SshPort: credentials.Port,
+ SshAuthMethod: credentials.AuthMethod,
+ SshUsername: credentials.Username,
+ SshPassword: credentials.Password,
+ SshKey: credentials.Key,
+ SshKeyPassphrase: credentials.KeyPassphrase,
+ SshKeyCertificate: credentials.KeyCertificate,
},
JumpServers: jumpServers,
UseSCP: xmaps.GetBool(options.ProviderExtendedConfig, "useSCP"),
diff --git a/internal/domain/access.go b/internal/domain/access.go
index ae5b67167..1ee3041ba 100644
--- a/internal/domain/access.go
+++ b/internal/domain/access.go
@@ -602,21 +602,23 @@ type AccessConfigForSpaceship struct {
}
type AccessConfigForSSH struct {
- Host string `json:"host"`
- Port int32 `json:"port"`
- AuthMethod string `json:"authMethod"`
- Username string `json:"username"`
- Password string `json:"password,omitempty"`
- Key string `json:"key,omitempty"`
- KeyPassphrase string `json:"keyPassphrase,omitempty"`
- JumpServers []struct {
- Host string `json:"host"`
- Port int32 `json:"port"`
- AuthMethod string `json:"authMethod"`
- Username string `json:"username"`
- Password string `json:"password,omitempty"`
- Key string `json:"key,omitempty"`
- KeyPassphrase string `json:"keyPassphrase,omitempty"`
+ Host string `json:"host"`
+ Port int32 `json:"port"`
+ AuthMethod string `json:"authMethod"`
+ Username string `json:"username"`
+ Password string `json:"password,omitempty"`
+ Key string `json:"key,omitempty"`
+ KeyPassphrase string `json:"keyPassphrase,omitempty"`
+ KeyCertificate string `json:"keyCertificate,omitempty"`
+ JumpServers []struct {
+ Host string `json:"host"`
+ Port int32 `json:"port"`
+ AuthMethod string `json:"authMethod"`
+ Username string `json:"username"`
+ Password string `json:"password,omitempty"`
+ Key string `json:"key,omitempty"`
+ KeyPassphrase string `json:"keyPassphrase,omitempty"`
+ KeyCertificate string `json:"keyCertificate,omitempty"`
} `json:"jumpServers,omitempty"`
}
diff --git a/internal/tools/ssh/client.go b/internal/tools/ssh/client.go
index abaf025b1..93a998a10 100644
--- a/internal/tools/ssh/client.go
+++ b/internal/tools/ssh/client.go
@@ -191,19 +191,14 @@ func createSshClientWithConn(config *ServerConfig, conn net.Conn) (*ssh.Client,
key := config.Key
keyPassphrase := config.KeyPassphrase
+ keyCertificate := config.KeyCertificate
- var signer ssh.Signer
- var err error
- if keyPassphrase != "" {
- signer, err = ssh.ParsePrivateKeyWithPassphrase([]byte(key), []byte(keyPassphrase))
- } else {
- signer, err = ssh.ParsePrivateKey([]byte(key))
- }
+ signers, err := createSigners(key, keyPassphrase, keyCertificate)
if err != nil {
return nil, err
}
- authMethods = append(authMethods, ssh.PublicKeys(signer))
+ authMethods = append(authMethods, ssh.PublicKeys(signers...))
}
default:
@@ -223,6 +218,44 @@ func createSshClientWithConn(config *ServerConfig, conn net.Conn) (*ssh.Client,
return ssh.NewClient(sshConn, chans, reqs), nil
}
+func createSigners(key, keyPassphrase, keyCertificate string) ([]ssh.Signer, error) {
+ var signer ssh.Signer
+ var err error
+ if keyPassphrase != "" {
+ signer, err = ssh.ParsePrivateKeyWithPassphrase([]byte(key), []byte(keyPassphrase))
+ } else {
+ signer, err = ssh.ParsePrivateKey([]byte(key))
+ }
+ if err != nil {
+ return nil, err
+ }
+
+ keyCertificate = strings.TrimSpace(keyCertificate)
+ if keyCertificate == "" {
+ return []ssh.Signer{signer}, nil
+ }
+
+ parsed, _, _, _, err := ssh.ParseAuthorizedKey([]byte(keyCertificate))
+ if err != nil {
+ return nil, fmt.Errorf("ssh: failed to parse key certificate: %w", err)
+ }
+
+ cert, ok := parsed.(*ssh.Certificate)
+ if !ok {
+ return nil, fmt.Errorf("ssh: key certificate is not an OpenSSH certificate")
+ }
+ if cert.CertType != ssh.UserCert {
+ return nil, fmt.Errorf("ssh: key certificate is not a user certificate")
+ }
+
+ certSigner, err := ssh.NewCertSigner(cert, signer)
+ if err != nil {
+ return nil, fmt.Errorf("ssh: failed to create key certificate signer: %w", err)
+ }
+
+ return []ssh.Signer{certSigner, signer}, nil
+}
+
func resolveAddr(host string, port int) string {
if port == 0 {
port = defaultPort
diff --git a/internal/tools/ssh/config.go b/internal/tools/ssh/config.go
index 4f62bc764..2645b266a 100644
--- a/internal/tools/ssh/config.go
+++ b/internal/tools/ssh/config.go
@@ -7,13 +7,14 @@ const (
)
type ServerConfig struct {
- Host string
- Port int
- AuthMethod AuthMethodType
- Username string
- Password string
- Key string
- KeyPassphrase string
+ Host string
+ Port int
+ AuthMethod AuthMethodType
+ Username string
+ Password string
+ Key string
+ KeyPassphrase string
+ KeyCertificate string
}
type Config struct {
diff --git a/pkg/core/certifier/challengers/http01/ssh/internal/ssh.go b/pkg/core/certifier/challengers/http01/ssh/internal/ssh.go
index d52218f55..bfa6f941b 100644
--- a/pkg/core/certifier/challengers/http01/ssh/internal/ssh.go
+++ b/pkg/core/certifier/challengers/http01/ssh/internal/ssh.go
@@ -104,6 +104,7 @@ func (p *HTTPProvider) createSshClient() (*ssh.Client, error) {
clientCfg.Password = p.config.Password
clientCfg.Key = p.config.Key
clientCfg.KeyPassphrase = p.config.KeyPassphrase
+ clientCfg.KeyCertificate = p.config.KeyCertificate
for _, jumpServer := range p.config.JumpServers {
jumpServerCfg := ssh.NewServerConfig()
jumpServerCfg.Host = jumpServer.Host
@@ -113,6 +114,7 @@ func (p *HTTPProvider) createSshClient() (*ssh.Client, error) {
jumpServerCfg.Password = jumpServer.Password
jumpServerCfg.Key = jumpServer.Key
jumpServerCfg.KeyPassphrase = jumpServer.KeyPassphrase
+ jumpServerCfg.KeyCertificate = jumpServer.KeyCertificate
clientCfg.JumpServers = append(clientCfg.JumpServers, *jumpServerCfg)
}
diff --git a/pkg/core/certifier/challengers/http01/ssh/ssh.go b/pkg/core/certifier/challengers/http01/ssh/ssh.go
index 18cfd5003..0252cda9b 100644
--- a/pkg/core/certifier/challengers/http01/ssh/ssh.go
+++ b/pkg/core/certifier/challengers/http01/ssh/ssh.go
@@ -26,7 +26,8 @@ type ServerConfig struct {
// SSH 登录私钥。
SshKey string `json:"sshKey,omitempty"`
// SSH 登录私钥口令。
- SshKeyPassphrase string `json:"sshKeyPassphrase,omitempty"`
+ SshKeyPassphrase string `json:"sshKeyPassphrase,omitempty"`
+ SshKeyCertificate string `json:"sshKeyCertificate,omitempty"`
}
type ChallengerConfig struct {
@@ -53,15 +54,17 @@ func NewChallenger(config *ChallengerConfig) (core.ACMEChallenger, error) {
providerConfig.Password = config.SshPassword
providerConfig.Key = config.SshKey
providerConfig.KeyPassphrase = config.SshKeyPassphrase
+ providerConfig.KeyCertificate = config.SshKeyCertificate
for _, jumpServer := range config.JumpServers {
jumpServerCfg := ssh.ServerConfig{
- Host: jumpServer.SshHost,
- Port: int(jumpServer.SshPort),
- AuthMethod: ssh.AuthMethodType(jumpServer.SshAuthMethod),
- Username: jumpServer.SshUsername,
- Password: jumpServer.SshPassword,
- Key: jumpServer.SshKey,
- KeyPassphrase: jumpServer.SshKeyPassphrase,
+ Host: jumpServer.SshHost,
+ Port: int(jumpServer.SshPort),
+ AuthMethod: ssh.AuthMethodType(jumpServer.SshAuthMethod),
+ Username: jumpServer.SshUsername,
+ Password: jumpServer.SshPassword,
+ Key: jumpServer.SshKey,
+ KeyPassphrase: jumpServer.SshKeyPassphrase,
+ KeyCertificate: jumpServer.SshKeyCertificate,
}
providerConfig.JumpServers = append(providerConfig.JumpServers, jumpServerCfg)
}
diff --git a/pkg/core/deployer/providers/ssh/ssh.go b/pkg/core/deployer/providers/ssh/ssh.go
index 9b19d1483..391dbb69a 100644
--- a/pkg/core/deployer/providers/ssh/ssh.go
+++ b/pkg/core/deployer/providers/ssh/ssh.go
@@ -36,7 +36,8 @@ type ServerConfig struct {
// SSH 登录私钥。
SshKey string `json:"sshKey,omitempty"`
// SSH 登录私钥口令。
- SshKeyPassphrase string `json:"sshKeyPassphrase,omitempty"`
+ SshKeyPassphrase string `json:"sshKeyPassphrase,omitempty"`
+ SshKeyCertificate string `json:"sshKeyCertificate,omitempty"`
}
type DeployerConfig struct {
@@ -257,6 +258,7 @@ func createSshClient(config DeployerConfig) (*ssh.Client, error) {
clientCfg.Password = config.SshPassword
clientCfg.Key = config.SshKey
clientCfg.KeyPassphrase = config.SshKeyPassphrase
+ clientCfg.KeyCertificate = config.SshKeyCertificate
for _, jumpServer := range config.JumpServers {
jumpServerCfg := ssh.NewServerConfig()
jumpServerCfg.Host = jumpServer.SshHost
@@ -266,6 +268,7 @@ func createSshClient(config DeployerConfig) (*ssh.Client, error) {
jumpServerCfg.Password = jumpServer.SshPassword
jumpServerCfg.Key = jumpServer.SshKey
jumpServerCfg.KeyPassphrase = jumpServer.SshKeyPassphrase
+ jumpServerCfg.KeyCertificate = jumpServer.SshKeyCertificate
clientCfg.JumpServers = append(clientCfg.JumpServers, *jumpServerCfg)
}
diff --git a/ui/src/components/access/forms/AccessConfigFieldsProviderSSH.tsx b/ui/src/components/access/forms/AccessConfigFieldsProviderSSH.tsx
index 6992d35c5..4c59ec02c 100644
--- a/ui/src/components/access/forms/AccessConfigFieldsProviderSSH.tsx
+++ b/ui/src/components/access/forms/AccessConfigFieldsProviderSSH.tsx
@@ -81,6 +81,16 @@ const AccessConfigFormFieldsProviderSSH = ({ disabled }: { disabled?: boolean })
>