fix: upgrade ssh keypair to ed25519 (#25738)

Co-authored-by: Qiu Jian <qiujian@yunionyun.com>
This commit is contained in:
Jian Qiu
2026-09-27 20:59:49 +08:00
committed by GitHub
co-authored by Qiu Jian
parent 07e2cdfde1
commit a6dc2f471f
27 changed files with 427 additions and 280 deletions
+22 -5
View File
@@ -578,11 +578,11 @@ func init() {
}
R(&HostSSHLoginOptions{}, "host-ssh", "SSH login of a host", func(s *mcclient.ClientSession, args *HostSSHLoginOptions) error {
i, e := modules.Hosts.PerformAction(s, args.ID, "login-info", nil)
privateKey := ""
var privateKeys []string
if e != nil {
if httputils.ErrorCode(e) == 404 || strings.Contains(e.Error(), "ciphertext too short") {
var err error
privateKey, err = modules.Sshkeypairs.FetchPrivateKeyBySession(context.Background(), s)
privateKeys, err = modules.Sshkeypairs.FetchAdminPrivateKeysBySession(context.Background(), s)
if err != nil {
return errors.Wrap(err, "fetch private key")
}
@@ -624,10 +624,27 @@ func init() {
if args.Port != 22 {
port = args.Port
}
sshCli, err := ssh.NewClient(host, port, user, passwd, privateKey)
if err != nil {
return err
if len(privateKeys) == 0 {
privateKeys = append(privateKeys, "")
}
var sshCli *ssh.Client
var errs []error
for _, privateKey := range privateKeys {
cli, err := ssh.NewClient(host, port, user, passwd, privateKey)
if err == nil {
sshCli = cli
break
} else {
errs = append(errs, err)
}
}
if sshCli == nil {
return errors.NewAggregate(errs)
}
log.Infof("ssh %s:%d", host, port)
if err := sshCli.RunTerminal(); err != nil {
return err
+20 -25
View File
@@ -24,6 +24,7 @@ import (
"path/filepath"
"gopkg.in/yaml.v2"
"k8s.io/apimachinery/pkg/util/errors"
"yunion.io/x/jsonutils"
"yunion.io/x/log"
@@ -826,15 +827,15 @@ func init() {
return fmt.Errorf("Not found ip address from server %s", opts.ID)
}
privateKey := ""
var privateKeys []string
params := jsonutils.NewDict()
if len(opts.Key) > 0 {
key, e := ioutil.ReadFile(opts.Key)
key, e := os.ReadFile(opts.Key)
if e != nil {
return e
}
params.Add(jsonutils.NewString(string(key)), "private_key")
privateKey = string(key)
privateKeys = append(privateKeys, string(key))
}
i, e := modules.Servers.PerformAction(s, srvid, "login-info", params)
@@ -873,6 +874,7 @@ func init() {
if err != nil {
return err
}
defer closeForward(s, srvid, forwardItem)
host = forwardItem.ProxyAddr
port = forwardItem.ProxyPort
}
@@ -881,7 +883,7 @@ func init() {
if opts.UseCloudroot {
var err error
privateKey, err = modules.Sshkeypairs.FetchPrivateKeyBySession(context.Background(), s)
privateKeys, err = modules.Sshkeypairs.FetchProjectPrivateKeysBySession(context.Background(), s)
if err != nil {
return err
}
@@ -889,30 +891,26 @@ func init() {
user = "cloudroot"
}
if len(privateKeys) == 0 {
privateKeys = append(privateKeys, "")
}
var sshCli *ssh.Client
err = nil
for ; sshCli == nil; sshCli, err = ssh.NewClient(host, port, user, passwd, privateKey) {
var errs []error
for _, privateKey := range privateKeys {
cli, err := ssh.NewClient(host, port, user, passwd, privateKey)
if err == nil {
continue
}
if opts.Host != "" {
return err
}
if forwardItem != nil {
closeForward(s, srvid, forwardItem)
return err
sshCli = cli
break
} else {
if vpcid != "default" {
forwardItem, e = openForward(s, srvid)
if e != nil {
return e
}
host = forwardItem.ProxyAddr
port = forwardItem.ProxyPort
}
errs = append(errs, err)
}
}
if sshCli == nil {
return errors.NewAggregate(errs)
}
log.Infof("ssh %s:%d", host, port)
if err := sshCli.RunTerminal(); err != nil {
if forwardItem != nil {
@@ -921,9 +919,6 @@ func init() {
return err
}
if forwardItem != nil {
closeForward(s, srvid, forwardItem)
}
return nil
})
}
+35 -42
View File
@@ -15,58 +15,49 @@
package compute
import (
"context"
"fmt"
"os"
"path"
"strings"
"yunion.io/x/jsonutils"
"yunion.io/x/pkg/errors"
"yunion.io/x/onecloud/pkg/mcclient"
"yunion.io/x/onecloud/pkg/mcclient/models"
modules "yunion.io/x/onecloud/pkg/mcclient/modules/compute"
"yunion.io/x/onecloud/pkg/util/procutils"
)
type SshkeypairQueryOptions struct {
Project string `help:"get keypair for specific project"`
Admin bool `help:"get admin keypair, sysadmin ONLY option"`
}
func getSshKeypair(s *mcclient.ClientSession, args *SshkeypairQueryOptions) ([]models.SshKeypair, error) {
ctx := context.Background()
if len(args.Project) > 0 {
return modules.Sshkeypairs.FetchKeypairsByProject(ctx, s, args.Project)
} else if args.Admin {
return modules.Sshkeypairs.FetchAdminKeypairsBySession(ctx, s)
} else {
return modules.Sshkeypairs.FetchProjectKeypairsBySession(ctx, s)
}
}
func init() {
type SshkeypairQueryOptions struct {
Project string `help:"get keypair for specific project"`
Admin bool `help:"get admin keypair, sysadmin ONLY option"`
}
getSshKeypair := func(s *mcclient.ClientSession, args *SshkeypairQueryOptions) (string, string, error) {
query := jsonutils.NewDict()
if args.Admin {
query.Add(jsonutils.JSONTrue, "admin")
}
var keys jsonutils.JSONObject
if len(args.Project) == 0 {
listResult, err := modules.Sshkeypairs.List(s, query)
if err != nil {
return "", "", err
}
keys = listResult.Data[0]
} else {
result, err := modules.Sshkeypairs.GetById(s, args.Project, query)
if err != nil {
return "", "", err
}
keys = result
}
privKey, _ := keys.GetString("private_key")
pubKey, _ := keys.GetString("public_key")
return privKey, pubKey, nil
}
R(&SshkeypairQueryOptions{}, "sshkeypair-show", "Get ssh keypairs", func(s *mcclient.ClientSession, args *SshkeypairQueryOptions) error {
privKey, pubKey, err := getSshKeypair(s, args)
keypairs, err := getSshKeypair(s, args)
if err != nil {
return err
}
fmt.Print(privKey)
fmt.Print(pubKey)
if len(keypairs) == 0 {
return errors.Wrap(errors.ErrNotFound, "no ssh key found")
}
for _, keypair := range keypairs {
fmt.Print(keypair.PrivateKey)
fmt.Print(keypair.PublicKey)
}
return nil
})
@@ -75,10 +66,13 @@ func init() {
TargetDir string `help:"Target directory to save cloud ssh keypair"`
}
R(&SshkeypairInjectOptions{}, "sshkeypair-inject", "Inject ssh keypairs to local path", func(s *mcclient.ClientSession, args *SshkeypairInjectOptions) error {
_, pubKey, err := getSshKeypair(s, &args.SshkeypairQueryOptions)
keypairs, err := getSshKeypair(s, &args.SshkeypairQueryOptions)
if err != nil {
return err
}
if len(keypairs) == 0 {
return errors.Wrap(errors.ErrNotFound, "no ssh key found")
}
targetDir := args.TargetDir
if targetDir == "" {
homeDir, err := os.UserHomeDir()
@@ -100,7 +94,7 @@ func init() {
}
oldKeys = string(output)
}
var MergeAuthorizedKeys = func(oldKeys string, pubKey string) string {
var MergeAuthorizedKeys = func(oldKeys string, keyPairs []models.SshKeypair) string {
const sshKeySignature = "@yunioncloudpods"
var allkeys = make(map[string]string)
if len(oldKeys) > 0 {
@@ -118,10 +112,9 @@ func init() {
}
}
}
candiateKeys := []string{pubKey}
for _, k := range candiateKeys {
if len(k) > 0 {
k = strings.TrimSpace(k)
for _, k := range keyPairs {
if len(k.PublicKey) > 0 {
k := strings.TrimSpace(k.PublicKey)
dat := strings.Split(k, " ")
if len(dat) > 1 {
if _, ok := allkeys[dat[1]]; !ok {
@@ -137,7 +130,7 @@ func init() {
return strings.Join(keys, "\n") + "\n"
}
newKeys := MergeAuthorizedKeys(oldKeys, pubKey)
newKeys := MergeAuthorizedKeys(oldKeys, keypairs)
if output, err := procutils.NewCommand(
"sh", "-c", fmt.Sprintf("echo '%s' > %s", newKeys, authFile)).Output(); err != nil {
return errors.Wrapf(err, "write public keys: %s", output)
@@ -125,11 +125,11 @@ func (ai *SAnsiblePlaybookInstance) runPlaybook(ctx context.Context, userCred mc
ar = obj.(*SAnsiblePlaybookReference)
}
var (
privateKey string
err error
privateKeys []string
err error
)
if privateKey, err = compute.Sshkeypairs.FetchPrivateKey(ctx, userCred); err != nil {
return err
if privateKeys, err = compute.Sshkeypairs.FetchProjectPrivateKeys(ctx, userCred); err != nil {
return errors.Wrap(err, "unable to fetch private keys")
}
_, err = db.Update(ai, func() error {
ai.StartTime = time.Now()
@@ -157,7 +157,7 @@ func (ai *SAnsiblePlaybookInstance) runPlaybook(ctx context.Context, userCred mc
}
sess := ansiblev2.NewOfflineSession().
Inventory(ai.Inventory).
PrivateKey(privateKey).
PrivateKeys(privateKeys).
ConfigYaml(params.YAMLString()).
PlaybookPath(ar.PlaybookPath).
OutputWriter(&ansiblePlaybookOutputWriter{ai}).
+4 -4
View File
@@ -276,11 +276,11 @@ func (apb *SAnsiblePlaybook) runPlaybook(ctx context.Context, userCred mcclient.
if err := ansible.ValidatePlaybook(pb); err != nil {
return err
}
if len(pb.PrivateKey) == 0 {
if k, err := compute.Sshkeypairs.FetchPrivateKey(ctx, userCred); err != nil {
return err
if len(pb.PrivateKeys) == 0 {
if keys, err := compute.Sshkeypairs.FetchProjectPrivateKeys(ctx, userCred); err != nil {
return errors.Wrap(err, "unable to fetch private keys")
} else {
pb.PrivateKey = []byte(k)
pb.PrivateKeys = keys
}
}
// init tmpdir clean policy
@@ -252,9 +252,9 @@ func (apb *SAnsiblePlaybookV2) runPlaybook(ctx context.Context, userCred mcclien
// time.Sleep(50 * time.Second)
var (
privateKey string
err error
files = map[string][]byte{}
privateKeys []string
err error
files = map[string][]byte{}
)
if apb.Files != "" {
obj, err := jsonutils.ParseString(apb.Files)
@@ -274,7 +274,7 @@ func (apb *SAnsiblePlaybookV2) runPlaybook(ctx context.Context, userCred mcclien
}
}
// init private key
if privateKey, err = compute.Sshkeypairs.FetchPrivateKey(ctx, userCred); err != nil {
if privateKeys, err = compute.Sshkeypairs.FetchProjectPrivateKeys(ctx, userCred); err != nil {
return errors.Wrap(err, "fetch private key")
}
@@ -306,7 +306,7 @@ func (apb *SAnsiblePlaybookV2) runPlaybook(ctx context.Context, userCred mcclien
sess := ansiblev2.NewSession().
Inventory(apb.Inventory).
Playbook(apb.Playbook).
PrivateKey(privateKey).
PrivateKeys(privateKeys).
Requirements(requirements).
Files(files).
OutputWriter(&ansiblePlaybookOutputWriter{apb}).
@@ -91,9 +91,9 @@ func (v *ValidatorAnsiblePlaybook) Validate(ctx context.Context, data *jsonutils
return httperrors.NewInputParameterError("%s", err.Error())
}
// add LF for privateKey
if len(pb.PrivateKey) > 0 && pb.PrivateKey[len(pb.PrivateKey)-1] != 10 {
pb.PrivateKey = append(pb.PrivateKey, 10)
}
// if len(pb.PrivateKey) > 0 && pb.PrivateKey[len(pb.PrivateKey)-1] != 10 {
// pb.PrivateKey = append(pb.PrivateKey, 10)
//}
pbJson := jsonutils.Marshal(pb)
if serialized := pbJson.String(); len(serialized) > PlaybookMaxBytes {
return httperrors.NewBadRequestError("playbook too big, got %d bytes, exceeding %d",
+9 -7
View File
@@ -1636,7 +1636,7 @@ func (b *SBaremetalInstance) GetServerSSHClient() (*ssh.Client, error) {
return nil, errors.Error("No server")
}
privateKey, err := modules.Sshkeypairs.FetchPrivateKey(context.TODO(), auth.AdminCredential())
privateKeys, err := modules.Sshkeypairs.FetchProjectPrivateKeys(context.TODO(), auth.AdminCredential())
if err != nil {
return nil, errors.Wrapf(err, "Get server %s login info", s.GetId())
}
@@ -1645,12 +1645,14 @@ func (b *SBaremetalInstance) GetServerSSHClient() (*ssh.Client, error) {
for idx, nic := range nics {
if nic.Ip != "" {
for _, user := range []string{"cloudroot", "root"} {
sshCli, err := ssh.NewClient(nic.Ip, 22, user, "", privateKey)
if err != nil {
err = errors.Wrapf(err, "New server %s ssh client %s@%s", s.GetName(), user, nic.Ip)
errs = append(errs, err)
} else {
return sshCli, nil
for _, privateKey := range privateKeys {
sshCli, err := ssh.NewClient(nic.Ip, 22, user, "", privateKey)
if err != nil {
err = errors.Wrapf(err, "New server %s ssh client %s@%s", s.GetName(), user, nic.Ip)
errs = append(errs, err)
} else {
return sshCli, nil
}
}
}
} else {
+39 -17
View File
@@ -35,9 +35,10 @@ import (
type SProxyEndpoint struct {
db.SVirtualResourceBase
User string `nullable:"false" list:"user" update:"user" create:"optional"`
Host string `nullable:"false" list:"user" update:"user" create:"required"`
Port int `nullable:"false" list:"user" update:"user" create:"optional"`
User string `nullable:"false" list:"user" update:"user" create:"optional"`
Host string `nullable:"false" list:"user" update:"user" create:"required"`
Port int `nullable:"false" list:"user" update:"user" create:"optional"`
PrivateKey string `nullable:"false" update:"user" list:"admin" get:"admin" create:"required"` // do not allow get, list
IntranetIpAddr string `width:"16" charset:"ascii" nullable:"true" list:"user" create:"required"`
@@ -73,7 +74,7 @@ func (man *SProxyEndpointManager) PerformCreateFromServer(ctx context.Context, u
if err != nil {
return nil, err
}
if serverInfo.PrivateKey == "" {
if len(serverInfo.PrivateKeys) == 0 {
return nil, httperrors.NewBadRequestError("cannot find ssh private key for this server")
}
@@ -111,21 +112,42 @@ func (man *SProxyEndpointManager) PerformCreateFromServer(ctx context.Context, u
if portStr, ok := serverInfo.Server.Metadata[compute_apis.SSH_PORT]; ok {
port, _ = strconv.Atoi(portStr)
}
proxyendpoint := &SProxyEndpoint{
User: "cloudroot",
Host: host,
Port: port,
PrivateKey: serverInfo.PrivateKey,
createAndVerifyPE := func(privateKey string) (*SProxyEndpoint, error) {
proxyendpoint := &SProxyEndpoint{
User: "cloudroot",
Host: host,
Port: port,
PrivateKey: privateKey,
IntranetIpAddr: nic.IpAddr,
IntranetIpAddr: nic.IpAddr,
}
proxyendpoint.SetModelManager(man, proxyendpoint)
proxyendpoint.Name = name
proxyendpoint.DomainId = serverInfo.Server.DomainId
proxyendpoint.ProjectId = serverInfo.Server.ProjectId
if err := proxyendpoint.remoteCheckMake(ctx, userCred); err != nil {
return nil, errors.Wrap(err, "remoteCheckMake")
}
return proxyendpoint, nil
}
proxyendpoint.SetModelManager(man, proxyendpoint)
proxyendpoint.Name = name
proxyendpoint.DomainId = serverInfo.Server.DomainId
proxyendpoint.ProjectId = serverInfo.Server.ProjectId
if err := proxyendpoint.remoteCheckMake(ctx, userCred); err != nil {
return nil, err
var proxyendpoint *SProxyEndpoint
var errs []error
for _, privateKey := range serverInfo.PrivateKeys {
pe, err := createAndVerifyPE(privateKey)
if err == nil {
proxyendpoint = pe
break
} else {
errs = append(errs, err)
}
}
if proxyendpoint == nil {
if len(errs) > 0 {
return nil, errors.NewAggregate(errs)
} else {
return nil, errors.Wrap(httperrors.ErrInvalidStatus, "server has no usable ssh private key")
}
}
if err := man.TableSpec().Insert(ctx, proxyendpoint); err != nil {
@@ -123,7 +123,7 @@ func (proxyendpoint *SProxyEndpoint) remoteConfigure(ctx context.Context, userCr
host.SetVar("ansible_port", fmt.Sprintf("%d", proxyendpoint.Port))
host.SetVar("ansible_become", "yes")
pb := &ansible.Playbook{
PrivateKey: []byte(proxyendpoint.PrivateKey),
PrivateKeys: []string{proxyendpoint.PrivateKey},
Inventory: ansible.Inventory{
Hosts: []ansible.Host{host},
},
+4 -4
View File
@@ -29,7 +29,7 @@ type serverInfo struct {
// PrivateKey is the one corresponds to userCred when getting this
// serverInfo instance. It can be empty
PrivateKey string
PrivateKeys []string
}
func (si *serverInfo) GetNic() *compute_apis.GuestnetworkShortDesc {
@@ -71,11 +71,11 @@ func getServerInfo(
return nil, httperrors.NewServerError("unmarshal server %s: %v", serverId, err)
}
privateKey, _ := compute_modules.Sshkeypairs.FetchPrivateKey(ctx, userCred)
privateKeys, _ := compute_modules.Sshkeypairs.FetchProjectPrivateKeys(ctx, userCred)
serverInfo := &serverInfo{
Server: server,
PrivateKey: privateKey,
Server: server,
PrivateKeys: privateKeys,
}
return serverInfo, nil
}
+29 -24
View File
@@ -87,11 +87,20 @@ func (guest *SGuest) GetDetailsSshable(
if err != nil {
return nil, httperrors.NewInternalServerError("fetch ssh private key: %v", err)
}
tryData.PrivateKey = privateKey
tryData.PublicKey = publicKey
var sshTryErrs []error
for i := range privateKey {
tryData.PrivateKey = privateKey[i]
tryData.PublicKey = publicKey[i]
if err := guest.sshableTryEach(ctx, userCred, tryData); err != nil {
return nil, err
if err := guest.sshableTryEach(ctx, userCred, tryData); err != nil {
sshTryErrs = append(sshTryErrs, err)
} else {
sshTryErrs = nil
break
}
}
if len(sshTryErrs) > 0 {
return nil, errors.NewAggregate(sshTryErrs)
}
{
@@ -497,7 +506,7 @@ func (guest *SGuest) PerformMakeSshable(
},
}
if input.PrivateKey != "" {
pb.PrivateKey = []byte(input.PrivateKey)
pb.PrivateKeys = []string{input.PrivateKey}
} else if input.Password != "" {
host.SetVar("ansible_password", input.Password)
}
@@ -537,8 +546,8 @@ func (guest *SGuest) GetDetailsMakeSshableCmd(
varVals := [][2]string{
{"user", "cloudroot"},
{"adminpub", strings.TrimSpace(adminPublicKey)},
{"projpub", strings.TrimSpace(projectPublicKey)},
{"adminpub", strings.TrimSpace(adminPublicKey[0])},
{"projpub", strings.TrimSpace(projectPublicKey[0])},
}
shellCmd := ""
for i := range varVals {
@@ -547,23 +556,18 @@ func (guest *SGuest) GetDetailsMakeSshableCmd(
}
shellCmd += `
group="$user"
sshdir="/home/$user/.ssh"
keyfile="$sshdir/authorized_keys"
`
shellCmd += `
id -g "$group" &>/dev/null || groupadd "$group"
id -u "$user" &>/dev/null || useradd --create-home --gid "$group" "$user"
mkdir -p "$sshdir"
grep -q -F "$adminpub" "$keyfile" &>/dev/null || echo "$adminpub" >>"$keyfile"
grep -q -F "$projpub" "$keyfile" &>/dev/null || echo "$projpub" >>"$keyfile"
chown -R "$user:$group" "$sshdir"
chmod -R 700 "$sshdir"
chmod -R 600 "$keyfile"
if ! grep -q "^$user " /etc/sudoers; then
echo "$user ALL=(ALL) NOPASSWD: ALL" | EDITOR='tee -a' visudo
fi
sshdir=/opt/$user/.ssh
keyfile=$sshdir/authorized_keys
grep -q "^$user:" /etc/group || groupadd "$user"
shell=$(command -v bash || command -v zsh || true)
id -u "$user" >/dev/null 2>&1 || useradd ${shell:+--shell "$shell"} -d $(dirname $sshdir) --create-home -g "$user" "$user"
install -d -m 700 -o "$user" -g "$user" "$sshdir"
for k in "$adminpub" "$projpub"; do
grep -qF "$k" "$keyfile" 2>/dev/null || echo "$k" >>"$keyfile"
done
chown "$user:$user" "$keyfile"
chmod 600 "$keyfile"
grep -q "^$user " /etc/sudoers || echo "$user ALL=(ALL) NOPASSWD: ALL" | EDITOR='tee -a' visudo
`
output = compute_api.GuestMakeSshableCmdOutput{
ShellCmd: shellCmd,
@@ -590,6 +594,7 @@ func (guest *SGuest) PerformSetSshport(ctx context.Context, userCred mcclient.To
}
return nil, guest.SetSshPort(ctx, userCred, input.Port)
}
func (guest *SGuest) GetDetailsSshport(
ctx context.Context,
userCred mcclient.TokenCredential,
+2 -2
View File
@@ -5707,8 +5707,8 @@ func (self *SGuest) GetDeployConfigOnHost(ctx context.Context, userCred mcclient
log.Errorf("fail to get ssh project public key %s", err)
}
config.Add(jsonutils.NewString(adminPubKey), "admin_public_key")
config.Add(jsonutils.NewString(projPubKey), "project_public_key")
config.Add(jsonutils.NewString(adminPubKey[0]), "admin_public_key")
config.Add(jsonutils.NewString(projPubKey[0]), "project_public_key")
config.Add(jsonutils.NewString(deployAction), "action")
+2 -1
View File
@@ -329,10 +329,11 @@ func (manager *SKubeNodePoolManager) ValidateCreateData(ctx context.Context, use
keypair := keypairObj.(*SKeypair)
input.PublicKey = keypair.PublicKey
} else {
_, input.PublicKey, err = sshkeys.GetSshAdminKeypair(ctx)
_, pubKeys, err := sshkeys.GetSshAdminKeypair(ctx)
if err != nil {
return nil, httperrors.NewGeneralError(errors.Wrapf(err, "GetSshAdminKeypair"))
}
input.PublicKey = pubKeys[0]
}
if input.DesiredInstanceCount > 0 {
+5 -1
View File
@@ -3247,7 +3247,11 @@ func (self *SManagedVirtualizationRegionDriver) RequestCreateKubeCluster(ctx con
opts.RoleName, _ = params.GetString("role_name")
opts.PrivateAccess, _ = params.Bool("private_access")
opts.PublicAccess, _ = params.Bool("public_access")
_, opts.PublicKey, _ = sshkeys.GetSshAdminKeypair(ctx)
_, pubKeys, err := sshkeys.GetSshAdminKeypair(ctx)
if err != nil {
return nil, errors.Wrapf(err, "GetSshAdminKeypair")
}
opts.PublicKey = pubKeys[0]
iregion, err := cluster.GetIRegion(ctx)
if err != nil {
+20 -3
View File
@@ -31,10 +31,15 @@ import (
"yunion.io/x/onecloud/pkg/httperrors"
"yunion.io/x/onecloud/pkg/mcclient"
"yunion.io/x/onecloud/pkg/mcclient/auth"
"yunion.io/x/onecloud/pkg/mcclient/models"
)
func AddSshKeysHandler(prefix string, app *appsrv.Application) {
// 1. 获取用户自己项目的密钥对,总是返回公钥+私钥
// 2. 如果是regionadmin所在系统项目(system),指定admin=true,则获取全局管理密钥
app.AddHandler2("GET", fmt.Sprintf("%s/sshkeypairs", prefix), auth.Authenticate(sshKeysHandler), nil, "get_sshkeys", nil)
// 1. 获取指定项目(tenant_id)的项目密钥对,只有域管理员或者系统管理员级别才能获得私钥
// 2. 无法获取全局管理密钥
app.AddHandler2("GET", fmt.Sprintf("%s/sshkeypairs/<tenant_id>", prefix), auth.Authenticate(adminSshKeysHandler), nil, "get_sshkeys", nil)
}
@@ -80,7 +85,7 @@ func sshKeysHandler(ctx context.Context, w http.ResponseWriter, r *http.Request)
}
func sendSshKey(ctx context.Context, w http.ResponseWriter, userCred mcclient.TokenCredential, projectId string, isAdmin bool, publicOnly bool) {
var privKey, pubKey string
var privKey, pubKey []string
if isAdmin {
if policy.PolicyManager.Allow(rbacscope.ScopeSystem, userCred, consts.GetServiceType(), "sshkeypairs", policy.PolicyActionGet).Result.IsAllow() {
@@ -93,12 +98,24 @@ func sendSshKey(ctx context.Context, w http.ResponseWriter, userCred mcclient.To
privKey, pubKey, _ = GetSshProjectKeypair(ctx, projectId)
}
keypairs := make([]models.SshKeypair, 0, len(privKey))
for i := range pubKey {
keypair := models.SshKeypair{
PublicKey: pubKey[i],
}
if !publicOnly {
keypair.PrivateKey = privKey[i]
}
keypairs = append(keypairs, keypair)
}
ret := jsonutils.NewDict()
if !publicOnly {
ret.Add(jsonutils.NewString(privKey), "private_key")
ret.Add(jsonutils.NewString(privKey[0]), "private_key")
}
ret.Add(jsonutils.NewString(pubKey), "public_key")
ret.Add(jsonutils.NewString(pubKey[0]), "public_key")
ret.Add(jsonutils.Marshal(keypairs), "keypairs")
body := jsonutils.NewDict()
body.Add(ret, "sshkeypair")
appsrv.SendJSON(w, body)
+41 -12
View File
@@ -16,6 +16,7 @@ package sshkeys
import (
"context"
"strings"
"yunion.io/x/pkg/utils"
@@ -25,14 +26,20 @@ import (
)
const (
sshAdminPrivateKey = "admin-ssh-private-key"
sshAdminPublicKey = "admin-ssh-public-key"
sshAdminRsaPrivateKey = "admin-ssh-private-key"
sshAdminRsaPublicKey = "admin-ssh-public-key"
sshPrivateKey = "project-ssh-private-key"
sshPublicKey = "project-ssh-public-key"
sshAdminEd25519PrivateKey = "admin-ssh-ed25519-private-key"
sshAdminEd25519PublicKey = "admin-ssh-ed25519-public-key"
sshRsaPrivateKey = "project-ssh-private-key"
sshRsaPublicKey = "project-ssh-public-key"
sshEd25519PrivateKey = "project-ssh-ed25519-private-key"
sshEd25519PublicKey = "project-ssh-ed25519-public-key"
)
func _getKeys(ctx context.Context, tenantId string, privateKey, publicKey string) (string, string, error) {
func _getKeys(ctx context.Context, tenantId string, privateKey, publicKey string, autocreate bool) (string, string, error) {
tenant, err := db.TenantCacheManager.FetchTenantById(ctx, tenantId)
if err != nil {
return "", "", err
@@ -40,21 +47,43 @@ func _getKeys(ctx context.Context, tenantId string, privateKey, publicKey string
private := tenant.GetMetadata(ctx, privateKey, nil)
public := tenant.GetMetadata(ctx, publicKey, nil)
userCred := auth.AdminCredential()
if len(private) == 0 || len(public) == 0 {
private, public, _ = seclib2.GenerateRSASSHKeypair()
if (len(private) == 0 || len(public) == 0) && autocreate {
if strings.Contains(privateKey, "ed25519") {
private, public, _ = seclib2.GenerateED25519SSHKeypair()
} else {
private, public, _ = seclib2.GenerateRSASSHKeypair()
}
private, _ = utils.EncryptAESBase64(tenantId, private)
tenant.SetMetadata(ctx, privateKey, private, userCred)
tenant.SetMetadata(ctx, publicKey, public, userCred)
} else if len(private) > 0 {
private, _ = utils.DescryptAESBase64(tenantId, private)
}
private, _ = utils.DescryptAESBase64(tenantId, private)
return private, public, nil
}
func GetSshProjectKeypair(ctx context.Context, tenantId string) (string, string, error) {
return _getKeys(ctx, tenantId, sshPrivateKey, sshPublicKey)
func GetSshProjectKeypair(ctx context.Context, tenantId string) ([]string, []string, error) {
return getSshProjectKeypair(ctx, tenantId, sshEd25519PrivateKey, sshEd25519PublicKey, sshRsaPrivateKey, sshRsaPublicKey)
}
func GetSshAdminKeypair(ctx context.Context) (string, string, error) {
func getSshProjectKeypair(ctx context.Context, tenantId string, ed25519PrivKey, ed25519PubKey, rsaPrivKey, rsaPubKey string) ([]string, []string, error) {
ed25519Priv, ed25519Pub, err := _getKeys(ctx, tenantId, ed25519PrivKey, ed25519PubKey, true)
if err != nil {
return nil, nil, err
}
rsaPriv, rsaPub, _ := _getKeys(ctx, tenantId, rsaPrivKey, rsaPubKey, false)
private := []string{ed25519Priv}
if len(rsaPriv) > 0 {
private = append(private, rsaPriv)
}
public := []string{ed25519Pub}
if len(rsaPub) > 0 {
public = append(public, rsaPub)
}
return private, public, nil
}
func GetSshAdminKeypair(ctx context.Context) ([]string, []string, error) {
userCred := auth.AdminCredential()
return _getKeys(ctx, userCred.GetProjectId(), sshAdminPrivateKey, sshAdminPublicKey)
return getSshProjectKeypair(ctx, userCred.GetProjectId(), sshAdminEd25519PrivateKey, sshAdminEd25519PublicKey, sshAdminRsaPrivateKey, sshAdminRsaPublicKey)
}
+3 -7
View File
@@ -277,18 +277,14 @@ func DeployAdminAuthorizedKeys(s *mcclient.ClientSession) error {
return errors.Wrapf(err, "mkdir .ssh %s", output)
}
query := jsonutils.NewDict()
query.Set("admin", jsonutils.JSONTrue)
ret, err := modules.Sshkeypairs.List(s, query)
keypairs, err := modules.Sshkeypairs.FetchAdminKeypairsBySession(s.GetContext(), s)
if err != nil {
return errors.Wrap(err, "modules.Sshkeypairs.List")
}
if len(ret.Data) == 0 {
if len(keypairs) == 0 {
return errors.Wrap(httperrors.ErrNotFound, "Not found admin sshkey")
}
keys := ret.Data[0]
adminPublicKey, _ := keys.GetString("public_key")
pubKeys := &deployapi.SSHKeys{AdminPublicKey: adminPublicKey}
pubKeys := &deployapi.SSHKeys{AdminPublicKey: keypairs[0].PublicKey}
var oldKeys string
authFile := path.Join(sshDir, "authorized_keys")
+2 -2
View File
@@ -15,6 +15,6 @@
package models
type SshKeypair struct {
PrivateKey string
PublicKey string
PrivateKey string `json:"private_key"`
PublicKey string `json:"public_key"`
}
+65 -23
View File
@@ -48,42 +48,84 @@ func (this *SSshkeypairManager) List(s *mcclient.ClientSession, params jsonutils
return &result, nil
}
func (this *SSshkeypairManager) FetchPrivateKey(ctx context.Context, userCred mcclient.TokenCredential) (string, error) {
func (this *SSshkeypairManager) FetchProjectPrivateKeys(ctx context.Context, userCred mcclient.TokenCredential) ([]string, error) {
s := auth.GetSession(ctx, userCred, "")
return this.FetchPrivateKeyBySession(ctx, s)
return this.FetchProjectPrivateKeysBySession(ctx, s)
}
func (this *SSshkeypairManager) FetchPrivateKeyBySession(ctx context.Context, s *mcclient.ClientSession) (string, error) {
kp, err := this.FetchKeypairBySession(ctx, s)
func (this *SSshkeypairManager) FetchProjectPrivateKeysBySession(ctx context.Context, s *mcclient.ClientSession) ([]string, error) {
kp, err := this.FetchProjectKeypairsBySession(ctx, s)
if err != nil {
return "", errors.Wrap(err, "FetchKeypairBySession")
return nil, errors.Wrap(err, "FetchProjectKeypairsBySession")
}
return kp.PrivateKey, nil
keys := make([]string, 0, len(kp))
for _, kp := range kp {
keys = append(keys, kp.PrivateKey)
}
return keys, nil
}
func (this *SSshkeypairManager) FetchKeypairBySession(ctx context.Context, s *mcclient.ClientSession) (*models.SshKeypair, error) {
userCred := s.GetToken()
func (this *SSshkeypairManager) FetchAdminPrivateKeysBySession(ctx context.Context, s *mcclient.ClientSession) ([]string, error) {
kp, err := this.FetchAdminKeypairsBySession(ctx, s)
if err != nil {
return nil, errors.Wrap(err, "FetchAdminKeypairsBySession")
}
keys := make([]string, 0, len(kp))
for _, kp := range kp {
keys = append(keys, kp.PrivateKey)
}
return keys, nil
}
func (this *SSshkeypairManager) FetchAdminKeypairsBySession(ctx context.Context, s *mcclient.ClientSession) ([]models.SshKeypair, error) {
return this.fetchKeypairsBySession(ctx, s, true)
}
func (this *SSshkeypairManager) FetchProjectKeypairsBySession(ctx context.Context, s *mcclient.ClientSession) ([]models.SshKeypair, error) {
return this.fetchKeypairsBySession(ctx, s, false)
}
func (this *SSshkeypairManager) fetchKeypairsBySession(ctx context.Context, s *mcclient.ClientSession, isAdmin bool) ([]models.SshKeypair, error) {
jd := jsonutils.NewDict()
var jr jsonutils.JSONObject
if userCred.HasSystemAdminPrivilege() {
if isAdmin {
jd.Set("admin", jsonutils.JSONTrue)
r, err := Sshkeypairs.List(s, jd)
if err != nil {
return nil, errors.Wrap(err, "get admin ssh key")
}
r, err := this.List(s, jd)
if err != nil {
return nil, errors.Wrap(err, "get admin ssh key")
}
if len(r.Data) == 0 {
return nil, errors.Wrap(errors.ErrNotFound, "no ssh key found")
}
return unmarshalSshKeypairs(r.Data[0])
}
func (this *SSshkeypairManager) FetchKeypairsByProject(ctx context.Context, s *mcclient.ClientSession, projectId string) ([]models.SshKeypair, error) {
jd := jsonutils.NewDict()
if len(projectId) == 0 {
projectId = s.GetProjectId()
}
r, err := Sshkeypairs.GetById(s, projectId, jd)
if err != nil {
return nil, errors.Wrap(err, "get project ssh key")
}
return unmarshalSshKeypairs(r)
}
func unmarshalSshKeypairs(jr jsonutils.JSONObject) ([]models.SshKeypair, error) {
kps := make([]models.SshKeypair, 0, 2)
if jr.Contains("keypairs") {
if err := jr.Unmarshal(&kps, "keypairs"); err != nil {
return nil, errors.Wrap(err, "unmarshal ssh key")
}
jr = r.Data[0]
} else {
r, err := Sshkeypairs.GetById(s, userCred.GetProjectId(), jd)
if err != nil {
return nil, errors.Wrap(err, "get project ssh key")
kp := models.SshKeypair{}
if err := jr.Unmarshal(&kp); err != nil {
return nil, errors.Wrap(err, "unmarshal ssh key")
}
jr = r
kps = append(kps, kp)
}
kp := &models.SshKeypair{}
if err := jr.Unmarshal(kp); err != nil {
return nil, errors.Wrap(err, "unmarshal ssh key")
}
return kp, nil
return kps, nil
}
var (
+39 -38
View File
@@ -17,7 +17,6 @@ package ansible
import (
"context"
"io"
"io/ioutil"
"os"
"os/exec"
"path/filepath"
@@ -51,10 +50,10 @@ func (pbs pbState) String() string {
}
type Playbook struct {
Inventory Inventory
Modules []Module
PrivateKey []byte
Files map[string][]byte
Inventory Inventory
Modules []Module
PrivateKeys []string
Files map[string][]byte
tmpdir string
noCleanOnExit bool
@@ -75,7 +74,7 @@ func (pb *Playbook) Copy() *Playbook {
pb1 := NewPlaybook()
pb1.Inventory = gotypes.DeepCopy(pb.Inventory).(Inventory)
pb1.Modules = gotypes.DeepCopy(pb.Modules).([]Module)
pb1.PrivateKey = gotypes.DeepCopy(pb.PrivateKey).([]byte)
pb1.PrivateKeys = gotypes.DeepCopy(pb.PrivateKeys).([]string)
pb1.Files = gotypes.DeepCopy(pb.Files).(map[string][]byte)
return pb1
}
@@ -102,8 +101,21 @@ func (pb *Playbook) Running() bool {
return pb.state == pbStateRunning
}
func (pb *Playbook) Run(ctx context.Context) error {
errs := make([]error, 0, len(pb.PrivateKeys))
for _, privateKey := range pb.PrivateKeys {
err := pb.runOnce(ctx, privateKey)
if err != nil {
errs = append(errs, err)
} else {
return nil
}
}
return errors.NewAggregate(errs)
}
// Run runs the playbook
func (pb *Playbook) Run(ctx context.Context) (err error) {
func (pb *Playbook) runOnce(ctx context.Context, privateKey string) error {
var (
tmpdir string
)
@@ -126,10 +138,9 @@ func (pb *Playbook) Run(ctx context.Context) (err error) {
}
// make tmpdir
tmpdir, err = os.MkdirTemp("", "onecloud-ansible")
tmpdir, err := os.MkdirTemp("", "onecloud-ansible")
if err != nil {
err = errors.Wrap(err, "making tmp dir")
return
return errors.Wrap(err, "making tmp dir")
}
pb.tmpdir = tmpdir
defer func() {
@@ -143,55 +154,45 @@ func (pb *Playbook) Run(ctx context.Context) (err error) {
// write out inventory
inventory := filepath.Join(tmpdir, "inventory")
err = ioutil.WriteFile(inventory, pb.Inventory.Data(), os.FileMode(0600))
err = os.WriteFile(inventory, pb.Inventory.Data(), os.FileMode(0600))
if err != nil {
err = errors.Wrapf(err, "writing inventory %s", inventory)
return
return errors.Wrapf(err, "writing inventory %s", inventory)
}
// write out private key
var privateKey string
if len(pb.PrivateKey) > 0 {
privateKey = filepath.Join(tmpdir, "private_key")
err = ioutil.WriteFile(privateKey, pb.PrivateKey, os.FileMode(0600))
var privateKeyFile string
if len(privateKey) > 0 {
privateKeyFile = filepath.Join(tmpdir, "private_key")
err = os.WriteFile(privateKeyFile, []byte(privateKey), os.FileMode(0600))
if err != nil {
err = errors.Wrapf(err, "writing private key %s", privateKey)
return
return errors.Wrapf(err, "writing private key %s", privateKeyFile)
}
}
// write out files
for name, content := range pb.Files {
path, err2 := fileutils2.JoinInside(tmpdir, name)
if err2 != nil {
err = errors.Wrapf(err2, "playbook file %s", name)
return
path, err := fileutils2.JoinInside(tmpdir, name)
if err != nil {
return errors.Wrapf(err, "playbook file %s", name)
}
dir := filepath.Dir(path)
err = os.MkdirAll(dir, os.FileMode(0700))
if err != nil {
err = errors.Wrapf(err, "mkdir -p %s", dir)
return
return errors.Wrapf(err, "mkdir -p %s", dir)
}
err = ioutil.WriteFile(path, content, os.FileMode(0600))
err = os.WriteFile(path, content, os.FileMode(0600))
if err != nil {
err = errors.Wrapf(err, "writing file %s", name)
return
return errors.Wrapf(err, "writing file %s", name)
}
}
// run modules one by one
var errs []error
defer func() {
if len(errs) > 0 {
err = errors.NewAggregate(errs)
}
}()
for _, m := range pb.Modules {
select {
case <-ctx.Done():
err = ctx.Err()
return
errs = append(errs, errors.Wrap(ctx.Err(), "context done"))
return errors.NewAggregate(errs)
default:
}
modArgs := strings.Join(m.Args, " ")
@@ -212,7 +213,7 @@ func (pb *Playbook) Run(ctx context.Context) (err error) {
stderr, _ := cmd.StderrPipe()
if err1 := cmd.Start(); err1 != nil {
errs = append(errs, errors.Wrapf(err1, "run module %q, args %q", m.Name, modArgs))
return
continue
}
// Mix stdout, stderr
if pb.outputWriter != nil {
@@ -221,10 +222,10 @@ func (pb *Playbook) Run(ctx context.Context) (err error) {
}
if err1 := cmd.Wait(); err1 != nil {
errs = append(errs, errors.Wrapf(err1, "wait module %q, args %q", m.Name, modArgs))
// continue to next
continue
}
}
return nil
return errors.NewAggregate(errs)
}
func (pb *Playbook) OutputWriter(w io.Writer) {
+29 -4
View File
@@ -17,6 +17,8 @@ package ansiblev2
import (
"context"
"io"
"yunion.io/x/pkg/errors"
)
type OfflineSession struct {
@@ -31,6 +33,16 @@ type OfflineSession struct {
configYaml string
}
type tryOfflineSession struct {
*OfflineSession
privateKeyIndex int
}
func (trySession *tryOfflineSession) GetPrivateKey() string {
return trySession.GetPrivateKeys()[trySession.privateKeyIndex]
}
func NewOfflineSession() *OfflineSession {
sess := &OfflineSession{
PlaybookSessionBase: NewPlaybookSessionBase(),
@@ -38,8 +50,8 @@ func NewOfflineSession() *OfflineSession {
return sess
}
func (sess *OfflineSession) PrivateKey(s string) *OfflineSession {
sess.privateKey = s
func (sess *OfflineSession) PrivateKeys(s []string) *OfflineSession {
sess.privateKeys = s
return sess
}
@@ -85,6 +97,19 @@ func (sess *OfflineSession) GetConfigYaml() string {
return sess.configYaml
}
func (sess *OfflineSession) Run(ctx context.Context) (err error) {
return runnable{sess}.Run(ctx)
func (sess *OfflineSession) Run(ctx context.Context) error {
privateKeys := sess.GetPrivateKeys()
errs := make([]error, 0, len(privateKeys))
for i := range privateKeys {
trySession := &tryOfflineSession{
OfflineSession: sess,
privateKeyIndex: i,
}
err := runnable{trySession}.Run(ctx)
if err == nil {
return nil
}
errs = append(errs, err)
}
return errors.NewAggregate(errs)
}
+3 -3
View File
@@ -262,7 +262,7 @@ func (r runnable) Run(ctx context.Context) (err error) {
}
type PlaybookSessionBase struct {
privateKey string
privateKeys []string
inventory string
outputWriter io.Writer
@@ -280,8 +280,8 @@ func NewPlaybookSessionBase() PlaybookSessionBase {
}
}
func (pb *PlaybookSessionBase) GetPrivateKey() string {
return pb.privateKey
func (pb *PlaybookSessionBase) GetPrivateKeys() []string {
return pb.privateKeys
}
func (pb *PlaybookSessionBase) IsKeepTmpdir() bool {
+29 -4
View File
@@ -17,6 +17,8 @@ package ansiblev2
import (
"context"
"io"
"yunion.io/x/pkg/errors"
)
type Session struct {
@@ -27,6 +29,16 @@ type Session struct {
files map[string][]byte
}
type trySession struct {
*Session
privateKeyIndex int
}
func (trySession *trySession) GetPrivateKey() string {
return trySession.GetPrivateKeys()[trySession.privateKeyIndex]
}
func NewSession() *Session {
sess := &Session{
PlaybookSessionBase: NewPlaybookSessionBase(),
@@ -35,8 +47,8 @@ func NewSession() *Session {
return sess
}
func (sess *Session) PrivateKey(s string) *Session {
sess.privateKey = s
func (sess *Session) PrivateKeys(s []string) *Session {
sess.privateKeys = s
return sess
}
@@ -103,6 +115,19 @@ func (sess *Session) GetFile() map[string][]byte {
return sess.files
}
func (sess *Session) Run(ctx context.Context) (err error) {
return runnable{sess}.Run(ctx)
func (sess *Session) Run(ctx context.Context) error {
privateKeys := sess.GetPrivateKeys()
errs := make([]error, 0, len(privateKeys))
for i := range privateKeys {
trySession := &trySession{
Session: sess,
privateKeyIndex: i,
}
err := runnable{trySession}.Run(ctx)
if err == nil {
return nil
}
errs = append(errs, err)
}
return errors.NewAggregate(errs)
}
+1 -1
View File
@@ -82,7 +82,7 @@ func NewClimcSshCommand(info *webconsole.ClimcSshInfo, s *mcclient.ClientSession
return nil, fmt.Errorf("Invalid username %q", info.Username)
}
targetIp := helper.FetchClimcTargetIp()
privateKey, err := helper.GetValidPrivateKey(targetIp, 22, info.Username, "")
privateKey, err := helper.GetValidPrivateKey(targetIp, 22, info.Username, s)
if err != nil {
return nil, errors.Wrap(err, "get cloud admin private key")
}
+9 -36
View File
@@ -27,6 +27,7 @@ import (
"yunion.io/x/pkg/errors"
"yunion.io/x/onecloud/pkg/httperrors"
"yunion.io/x/onecloud/pkg/mcclient"
"yunion.io/x/onecloud/pkg/mcclient/auth"
"yunion.io/x/onecloud/pkg/mcclient/modules/compute"
"yunion.io/x/onecloud/pkg/mcclient/modules/k8s"
@@ -78,45 +79,17 @@ func FetchClimcTargetIp() string {
return podIp
}
func GetValidPrivateKey(host string, port int, username string, projectId string) (string, error) {
func GetValidPrivateKey(host string, port int, username string, cliSession *mcclient.ClientSession) (string, error) {
errs := []error{}
ctx := context.Background()
admin := auth.GetAdminSession(ctx, o.Options.Region)
for _, gf := range []func() (jsonutils.JSONObject, error){
func() (jsonutils.JSONObject, error) {
if projectId == "" {
return nil, errors.Error("project_id is empty")
}
key, err := compute.Sshkeypairs.GetById(admin, projectId, jsonutils.Marshal(map[string]bool{"admin": true}))
if err != nil {
return nil, errors.Wrapf(err, "Sshkeypairs.GetById(%s)", projectId)
}
return key, nil
},
func() (jsonutils.JSONObject, error) {
query := jsonutils.NewDict()
query.Set("admin", jsonutils.JSONTrue)
ret, err := compute.Sshkeypairs.List(admin, query)
if err != nil {
return nil, errors.Wrap(err, "modules.Sshkeypairs.List")
}
if len(ret.Data) == 0 {
return nil, errors.Wrap(httperrors.ErrNotFound, "Not found admin sshkey")
}
keys := ret.Data[0]
return keys, nil
},
} {
key, err := gf()
if err != nil {
errs = append(errs, err)
continue
}
privKey, err := key.GetString("private_key")
if err != nil {
errs = append(errs, errors.Wrapf(err, "get private_key"))
continue
}
var privateKeys []string
projectPrivateKeys, _ := compute.Sshkeypairs.FetchProjectPrivateKeysBySession(ctx, cliSession)
privateKeys = append(privateKeys, projectPrivateKeys...)
adminPrivateKeys, _ := compute.Sshkeypairs.FetchAdminPrivateKeysBySession(ctx, admin)
privateKeys = append(privateKeys, adminPrivateKeys...)
for _, privKey := range privateKeys {
signer, err := ssh.ParsePrivateKey([]byte(privKey))
if err != nil {
errs = append(errs, errors.Wrapf(err, "ParsePrivateKey"))
+1 -1
View File
@@ -134,7 +134,7 @@ func (s *SSshSession) IsNeedLogin() (bool, error) {
return true, errors.Error("username is empty")
}
}
privateKey, err := helper.GetValidPrivateKey(s.Host, s.Port, s.Username, s.us.GetProjectId())
privateKey, err := helper.GetValidPrivateKey(s.Host, s.Port, s.Username, s.us)
if err != nil {
return true, errors.Wrap(err, "try to use cloud admin private_key for ssh login")
}