forked from hashicorp/terraform-provider-azurerm
/
ssh_keys.go
157 lines (131 loc) · 4.55 KB
/
ssh_keys.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
package compute
import (
"crypto/rsa"
"encoding/base64"
"fmt"
"regexp"
"strings"
"golang.org/x/crypto/ssh"
"github.com/Azure/azure-sdk-for-go/services/compute/mgmt/2019-12-01/compute"
"github.com/hashicorp/terraform-plugin-sdk/helper/schema"
"github.com/hashicorp/terraform-plugin-sdk/helper/validation"
"github.com/terraform-providers/terraform-provider-azurerm/azurerm/utils"
)
func SSHKeysSchema(isVirtualMachine bool) *schema.Schema {
// the SSH Keys for a Virtual Machine cannot be changed once provisioned:
// Code="PropertyChangeNotAllowed" Message="Changing property 'linuxConfiguration.ssh.publicKeys' is not allowed."
return &schema.Schema{
Type: schema.TypeSet,
Optional: true,
ForceNew: isVirtualMachine,
Elem: &schema.Resource{
Schema: map[string]*schema.Schema{
"public_key": {
Type: schema.TypeString,
Required: true,
ForceNew: isVirtualMachine,
ValidateFunc: ValidateSSHKey,
},
"username": {
Type: schema.TypeString,
Required: true,
ForceNew: isVirtualMachine,
ValidateFunc: validation.StringIsNotEmpty,
},
},
},
}
}
func ExpandSSHKeys(input []interface{}) []compute.SSHPublicKey {
output := make([]compute.SSHPublicKey, 0)
for _, v := range input {
raw := v.(map[string]interface{})
username := raw["username"].(string)
output = append(output, compute.SSHPublicKey{
KeyData: utils.String(raw["public_key"].(string)),
Path: utils.String(formatUsernameForAuthorizedKeysPath(username)),
})
}
return output
}
func FlattenSSHKeys(input *compute.SSHConfiguration) (*[]interface{}, error) {
if input == nil || input.PublicKeys == nil {
return &[]interface{}{}, nil
}
output := make([]interface{}, 0)
for _, v := range *input.PublicKeys {
if v.KeyData == nil || v.Path == nil {
continue
}
username := parseUsernameFromAuthorizedKeysPath(*v.Path)
if username == nil {
return nil, fmt.Errorf("Error parsing username from %q", *v.Path)
}
output = append(output, map[string]interface{}{
"public_key": *v.KeyData,
"username": *username,
})
}
return &output, nil
}
// formatUsernameForAuthorizedKeysPath returns the path to the authorized keys file
// for the specified username
func formatUsernameForAuthorizedKeysPath(username string) string {
return fmt.Sprintf("/home/%s/.ssh/authorized_keys", username)
}
// parseUsernameFromAuthorizedKeysPath parses the username out of the authorized keys
// path returned from the Azure API
func parseUsernameFromAuthorizedKeysPath(input string) *string {
// the Azure VM agent hard-codes this to `/home/username/.ssh/authorized_keys`
// as such we can hard-code this for a better UX
r := regexp.MustCompile("(/home/)+(?P<username>.*?)(/.ssh/authorized_keys)+")
keys := r.SubexpNames()
values := r.FindStringSubmatch(input)
if values == nil {
return nil
}
for i, k := range keys {
if k == "username" {
value := values[i]
return &value
}
}
return nil
}
// ValidateSSHKey performs some basic validation on supplied SSH Keys - Encoded Signature and Key Size are evaluated
// Will require rework if/when other Key Types are supported
func ValidateSSHKey(i interface{}, k string) (warnings []string, errors []error) {
v, ok := i.(string)
if !ok {
return nil, []error{fmt.Errorf("expected type of %q to be string", k)}
}
if strings.TrimSpace(v) == "" {
return nil, []error{fmt.Errorf("expected %q to not be an empty string or whitespace", k)}
}
keyParts := strings.Fields(v)
if len(keyParts) > 1 {
byteStr, err := base64.StdEncoding.DecodeString(keyParts[1])
if err != nil {
return nil, []error{fmt.Errorf("Error decoding %q for public key data", k)}
}
pubKey, err := ssh.ParsePublicKey(byteStr)
if err != nil {
return nil, []error{fmt.Errorf("Error parsing %q as a public key object", k)}
}
if pubKey.Type() != ssh.KeyAlgoRSA {
return nil, []error{fmt.Errorf("Error - the provided %s SSH key is not supported. Only RSA SSH keys are supported by Azure", pubKey.Type())}
} else {
rsaPubKey, ok := pubKey.(ssh.CryptoPublicKey).CryptoPublicKey().(*rsa.PublicKey)
if !ok {
return nil, []error{fmt.Errorf("Error - could not retrieve the RSA public key from the SSH public key")}
}
rsaPubKeyBits := rsaPubKey.Size() * 8
if rsaPubKeyBits < 2048 {
return nil, []error{fmt.Errorf("Error - the provided RSA SSH key has %d bits. Only ssh-rsa keys with 2048 bits or higher are supported by Azure", rsaPubKeyBits)}
}
}
} else {
return nil, []error{fmt.Errorf("Error %q is not a complete SSH2 Public Key", k)}
}
return warnings, errors
}