mirror of
https://github.com/certimate-go/certimate.git
synced 2026-10-16 15:16:05 +08:00
feat: ssh user certificate auth
This commit is contained in:
@@ -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"),
|
||||
|
||||
@@ -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"),
|
||||
|
||||
+17
-15
@@ -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"`
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
|
||||
@@ -81,6 +81,16 @@ const AccessConfigFormFieldsProviderSSH = ({ disabled }: { disabled?: boolean })
|
||||
>
|
||||
<Input.Password allowClear autoComplete="new-password" placeholder={t("access.form.ssh_key_passphrase.placeholder")} />
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item
|
||||
name={[parentNamePath, "keyCertificate"]}
|
||||
initialValue={initialValues.keyCertificate}
|
||||
label={t("access.form.ssh_key_certificate.label")}
|
||||
tooltip={<span dangerouslySetInnerHTML={{ __html: t("access.form.ssh_key_certificate.tooltip") }}></span>}
|
||||
rules={[formRule]}
|
||||
>
|
||||
<FileTextInput allowClear autoSize={{ minRows: 1, maxRows: 5 }} placeholder={t("access.form.ssh_key_certificate.placeholder")} />
|
||||
</Form.Item>
|
||||
</Show>
|
||||
|
||||
<Form.Item label={t("access.form.ssh_jump_servers.label")}>
|
||||
@@ -204,6 +214,17 @@ const AccessConfigFormFieldsProviderSSH = ({ disabled }: { disabled?: boolean })
|
||||
>
|
||||
<Input.Password allowClear autoComplete="new-password" placeholder={t("access.form.ssh_key_passphrase.placeholder")} />
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item
|
||||
name={[index, "keyCertificate"]}
|
||||
hidden={subfieldAuthMethod !== AUTH_METHOD_KEY}
|
||||
label={t("access.form.ssh_key_certificate.label")}
|
||||
tooltip={<span dangerouslySetInnerHTML={{ __html: t("access.form.ssh_key_certificate.tooltip") }}></span>}
|
||||
shouldUpdate
|
||||
rules={[formRule]}
|
||||
>
|
||||
<FileTextInput allowClear autoSize={{ minRows: 1, maxRows: 5 }} placeholder={t("access.form.ssh_key_certificate.placeholder")} />
|
||||
</Form.Item>
|
||||
</>
|
||||
),
|
||||
};
|
||||
@@ -254,6 +275,7 @@ const getSchema = ({ i18n = getI18n() }: { i18n: ReturnType<typeof getI18n> }) =
|
||||
password: z.string().nullish(),
|
||||
key: z.string().max(20480).nullish(),
|
||||
keyPassphrase: z.string().nullish(),
|
||||
keyCertificate: z.string().max(20480).nullish(),
|
||||
})
|
||||
.superRefine((values, ctx) => {
|
||||
switch (values.authMethod) {
|
||||
@@ -282,6 +304,15 @@ const getSchema = ({ i18n = getI18n() }: { i18n: ReturnType<typeof getI18n> }) =
|
||||
path: ["key"],
|
||||
});
|
||||
}
|
||||
|
||||
const keyCertificate = values.keyCertificate?.trim() ?? "";
|
||||
if (keyCertificate !== "" && !keyCertificate.includes("-cert-v01@openssh.com")) {
|
||||
ctx.addIssue({
|
||||
code: "custom",
|
||||
message: t("access.form.ssh_key_certificate.errmsg.invalid"),
|
||||
path: ["keyCertificate"],
|
||||
});
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
@@ -1433,6 +1433,14 @@
|
||||
"label": "SSH key passphrase (Optional)",
|
||||
"placeholder": "Please enter SSH key passphrase"
|
||||
},
|
||||
"ssh_key_certificate": {
|
||||
"label": "SSH key certificate (Optional)",
|
||||
"placeholder": "Please enter SSH key certificate\r\n\r\nssh-ed25519-cert-v01@openssh.com AAAA...",
|
||||
"tooltip": "The contents of an OpenSSH user certificate file (such as id_ed25519-cert.pub) signed by the user CA trusted by the target host. If set, it is offered before the bare SSH key during authentication.",
|
||||
"errmsg": {
|
||||
"invalid": "Invalid SSH key certificate"
|
||||
}
|
||||
},
|
||||
"ssh_jump_servers": {
|
||||
"label": "Jump servers (Optional)",
|
||||
"errmsg": {
|
||||
|
||||
@@ -1433,6 +1433,14 @@
|
||||
"label": "SSH 密钥口令(可选)",
|
||||
"placeholder": "请输入 SSH 密钥口令"
|
||||
},
|
||||
"ssh_key_certificate": {
|
||||
"label": "SSH 密钥证书(可选)",
|
||||
"placeholder": "请输入 SSH 密钥证书\r\n\r\nssh-ed25519-cert-v01@openssh.com AAAA...",
|
||||
"tooltip": "由目标主机信任的用户 CA 签发的 OpenSSH 用户证书文件内容(如 id_ed25519-cert.pub)。设置后将优先于裸私钥进行认证。",
|
||||
"errmsg": {
|
||||
"invalid": "无效的 SSH 密钥证书"
|
||||
}
|
||||
},
|
||||
"ssh_jump_servers": {
|
||||
"label": "跳板机(可选)",
|
||||
"errmsg": {
|
||||
|
||||
Reference in New Issue
Block a user