commit
32ae193e94
|
@ -0,0 +1,12 @@
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/hashicorp/terraform/builtin/providers/tls"
|
||||||
|
"github.com/hashicorp/terraform/plugin"
|
||||||
|
)
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
plugin.Serve(&plugin.ServeOpts{
|
||||||
|
ProviderFunc: tls.Provider,
|
||||||
|
})
|
||||||
|
}
|
|
@ -0,0 +1,111 @@
|
||||||
|
package tls
|
||||||
|
|
||||||
|
import (
|
||||||
|
"crypto/sha1"
|
||||||
|
"crypto/x509/pkix"
|
||||||
|
"encoding/hex"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/hashicorp/terraform/helper/schema"
|
||||||
|
"github.com/hashicorp/terraform/terraform"
|
||||||
|
)
|
||||||
|
|
||||||
|
func Provider() terraform.ResourceProvider {
|
||||||
|
return &schema.Provider{
|
||||||
|
ResourcesMap: map[string]*schema.Resource{
|
||||||
|
"tls_private_key": resourcePrivateKey(),
|
||||||
|
"tls_self_signed_cert": resourceSelfSignedCert(),
|
||||||
|
"tls_cert_request": resourceCertRequest(),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func hashForState(value string) string {
|
||||||
|
if value == "" {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
hash := sha1.Sum([]byte(strings.TrimSpace(value)))
|
||||||
|
return hex.EncodeToString(hash[:])
|
||||||
|
}
|
||||||
|
|
||||||
|
func nameFromResourceData(nameMap map[string]interface{}) (*pkix.Name, error) {
|
||||||
|
result := &pkix.Name{}
|
||||||
|
|
||||||
|
if value := nameMap["common_name"]; value != nil {
|
||||||
|
result.CommonName = value.(string)
|
||||||
|
}
|
||||||
|
if value := nameMap["organization"]; value != nil {
|
||||||
|
result.Organization = []string{value.(string)}
|
||||||
|
}
|
||||||
|
if value := nameMap["organizational_unit"]; value != nil {
|
||||||
|
result.OrganizationalUnit = []string{value.(string)}
|
||||||
|
}
|
||||||
|
if value := nameMap["street_address"]; value != nil {
|
||||||
|
valueI := value.([]interface{})
|
||||||
|
result.StreetAddress = make([]string, len(valueI))
|
||||||
|
for i, vi := range valueI {
|
||||||
|
result.StreetAddress[i] = vi.(string)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if value := nameMap["locality"]; value != nil {
|
||||||
|
result.Locality = []string{value.(string)}
|
||||||
|
}
|
||||||
|
if value := nameMap["province"]; value != nil {
|
||||||
|
result.Province = []string{value.(string)}
|
||||||
|
}
|
||||||
|
if value := nameMap["country"]; value != nil {
|
||||||
|
result.Country = []string{value.(string)}
|
||||||
|
}
|
||||||
|
if value := nameMap["postal_code"]; value != nil {
|
||||||
|
result.PostalCode = []string{value.(string)}
|
||||||
|
}
|
||||||
|
if value := nameMap["serial_number"]; value != nil {
|
||||||
|
result.SerialNumber = value.(string)
|
||||||
|
}
|
||||||
|
|
||||||
|
return result, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
var nameSchema *schema.Resource = &schema.Resource{
|
||||||
|
Schema: map[string]*schema.Schema{
|
||||||
|
"organization": &schema.Schema{
|
||||||
|
Type: schema.TypeString,
|
||||||
|
Optional: true,
|
||||||
|
},
|
||||||
|
"common_name": &schema.Schema{
|
||||||
|
Type: schema.TypeString,
|
||||||
|
Optional: true,
|
||||||
|
},
|
||||||
|
"organizational_unit": &schema.Schema{
|
||||||
|
Type: schema.TypeString,
|
||||||
|
Optional: true,
|
||||||
|
},
|
||||||
|
"street_address": &schema.Schema{
|
||||||
|
Type: schema.TypeList,
|
||||||
|
Optional: true,
|
||||||
|
Elem: &schema.Schema{
|
||||||
|
Type: schema.TypeString,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"locality": &schema.Schema{
|
||||||
|
Type: schema.TypeString,
|
||||||
|
Optional: true,
|
||||||
|
},
|
||||||
|
"province": &schema.Schema{
|
||||||
|
Type: schema.TypeString,
|
||||||
|
Optional: true,
|
||||||
|
},
|
||||||
|
"country": &schema.Schema{
|
||||||
|
Type: schema.TypeString,
|
||||||
|
Optional: true,
|
||||||
|
},
|
||||||
|
"postal_code": &schema.Schema{
|
||||||
|
Type: schema.TypeString,
|
||||||
|
Optional: true,
|
||||||
|
},
|
||||||
|
"serial_number": &schema.Schema{
|
||||||
|
Type: schema.TypeString,
|
||||||
|
Optional: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
|
@ -0,0 +1,36 @@
|
||||||
|
package tls
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/hashicorp/terraform/helper/schema"
|
||||||
|
"github.com/hashicorp/terraform/terraform"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestProvider(t *testing.T) {
|
||||||
|
if err := Provider().(*schema.Provider).InternalValidate(); err != nil {
|
||||||
|
t.Fatalf("err: %s", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var testProviders = map[string]terraform.ResourceProvider{
|
||||||
|
"tls": Provider(),
|
||||||
|
}
|
||||||
|
|
||||||
|
var testPrivateKey = `
|
||||||
|
-----BEGIN RSA PRIVATE KEY-----
|
||||||
|
MIICXAIBAAKBgQDPLaq43D9C596ko9yQipWUf2FbRhFs18D3wBDBqXLIoP7W3rm5
|
||||||
|
S292/JiNPa+mX76IYFF416zTBGG9J5w4d4VFrROn8IuMWqHgdXsCUf2szN7EnJcV
|
||||||
|
BsBzTxxWqz4DjX315vbm/PFOLlKzC0Ngs4h1iDiCD9Hk2MajZuFnJiqj1QIDAQAB
|
||||||
|
AoGAG6eQ3lQn7Zpd0cQ9sN2O0d+e8zwLH2g9TdTJZ9Bijf1Phwb764vyOQPGqTPO
|
||||||
|
unqVSEbzGRpQ62nuUf1zkOYDV+gKMNO3mj9Zu+qPNr/nQPHIaGZksPdD34qDUnBl
|
||||||
|
eRWVGNTyEGQsRPNN0RtFj8ifa4+OWiE30n95PBq2bUGZj4ECQQDZvS5X/4jYxnzw
|
||||||
|
CscaL4vO9OCVd/Fzdpfak0DQE/KCVmZxzcXu6Q8WuhybCynX84WKHQxuFAo+nBvr
|
||||||
|
kgtWXX7dAkEA85Vs5ehuDujBKCu3NJYI2R5ie49L9fEMFJVZK9FpkKacoAkET5BZ
|
||||||
|
UzaZrx4Fg3Zhcv1TssZKSyle+2lYiIydWQJBAMW8/aJi6WdcUsg4MXrBZSlsz6xO
|
||||||
|
AhOGxv90LS8KfnkJd/2wDyoZs19DY4kWSUjZ2hOEr+4j+u3DHcQAnJUxUW0CQGXP
|
||||||
|
DrUJcPbKUfF4VBqmmwwkpwT938Hr/iCcS6kE3hqXiN9a5XJb4vnk2FdZNPS9hf2J
|
||||||
|
5HHUbzj7EbgDT/3CyAECQG0qv6LNQaQMm2lmQKmqpi43Bqj9wvx0xGai1qCOvSeL
|
||||||
|
rpxCHbX0xSJh0s8j7exRHMF8W16DHjjkc265YdWPXWo=
|
||||||
|
-----END RSA PRIVATE KEY-----
|
||||||
|
`
|
|
@ -0,0 +1,135 @@
|
||||||
|
package tls
|
||||||
|
|
||||||
|
import (
|
||||||
|
"crypto/rand"
|
||||||
|
"crypto/x509"
|
||||||
|
"encoding/pem"
|
||||||
|
"fmt"
|
||||||
|
"net"
|
||||||
|
|
||||||
|
"github.com/hashicorp/terraform/helper/schema"
|
||||||
|
)
|
||||||
|
|
||||||
|
func resourceCertRequest() *schema.Resource {
|
||||||
|
return &schema.Resource{
|
||||||
|
Create: CreateCertRequest,
|
||||||
|
Delete: DeleteCertRequest,
|
||||||
|
Read: ReadCertRequest,
|
||||||
|
|
||||||
|
Schema: map[string]*schema.Schema{
|
||||||
|
|
||||||
|
"dns_names": &schema.Schema{
|
||||||
|
Type: schema.TypeList,
|
||||||
|
Optional: true,
|
||||||
|
Description: "List of DNS names to use as subjects of the certificate",
|
||||||
|
ForceNew: true,
|
||||||
|
Elem: &schema.Schema{
|
||||||
|
Type: schema.TypeString,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
"ip_addresses": &schema.Schema{
|
||||||
|
Type: schema.TypeList,
|
||||||
|
Optional: true,
|
||||||
|
Description: "List of IP addresses to use as subjects of the certificate",
|
||||||
|
ForceNew: true,
|
||||||
|
Elem: &schema.Schema{
|
||||||
|
Type: schema.TypeString,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
"key_algorithm": &schema.Schema{
|
||||||
|
Type: schema.TypeString,
|
||||||
|
Required: true,
|
||||||
|
Description: "Name of the algorithm to use to generate the certificate's private key",
|
||||||
|
ForceNew: true,
|
||||||
|
},
|
||||||
|
|
||||||
|
"private_key_pem": &schema.Schema{
|
||||||
|
Type: schema.TypeString,
|
||||||
|
Required: true,
|
||||||
|
Description: "PEM-encoded private key that the certificate will belong to",
|
||||||
|
ForceNew: true,
|
||||||
|
StateFunc: func(v interface{}) string {
|
||||||
|
return hashForState(v.(string))
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
"subject": &schema.Schema{
|
||||||
|
Type: schema.TypeList,
|
||||||
|
Required: true,
|
||||||
|
Elem: nameSchema,
|
||||||
|
ForceNew: true,
|
||||||
|
},
|
||||||
|
|
||||||
|
"cert_request_pem": &schema.Schema{
|
||||||
|
Type: schema.TypeString,
|
||||||
|
Computed: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func CreateCertRequest(d *schema.ResourceData, meta interface{}) error {
|
||||||
|
keyAlgoName := d.Get("key_algorithm").(string)
|
||||||
|
var keyFunc keyParser
|
||||||
|
var ok bool
|
||||||
|
if keyFunc, ok = keyParsers[keyAlgoName]; !ok {
|
||||||
|
return fmt.Errorf("invalid key_algorithm %#v", keyAlgoName)
|
||||||
|
}
|
||||||
|
keyBlock, _ := pem.Decode([]byte(d.Get("private_key_pem").(string)))
|
||||||
|
if keyBlock == nil {
|
||||||
|
return fmt.Errorf("no PEM block found in private_key_pem")
|
||||||
|
}
|
||||||
|
key, err := keyFunc(keyBlock.Bytes)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to decode private_key_pem: %s", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
subjectConfs := d.Get("subject").([]interface{})
|
||||||
|
if len(subjectConfs) != 1 {
|
||||||
|
return fmt.Errorf("must have exactly one 'subject' block")
|
||||||
|
}
|
||||||
|
subjectConf := subjectConfs[0].(map[string]interface{})
|
||||||
|
subject, err := nameFromResourceData(subjectConf)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("invalid subject block: %s", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
certReq := x509.CertificateRequest{
|
||||||
|
Subject: *subject,
|
||||||
|
}
|
||||||
|
|
||||||
|
dnsNamesI := d.Get("dns_names").([]interface{})
|
||||||
|
for _, nameI := range dnsNamesI {
|
||||||
|
certReq.DNSNames = append(certReq.DNSNames, nameI.(string))
|
||||||
|
}
|
||||||
|
ipAddressesI := d.Get("ip_addresses").([]interface{})
|
||||||
|
for _, ipStrI := range ipAddressesI {
|
||||||
|
ip := net.ParseIP(ipStrI.(string))
|
||||||
|
if ip == nil {
|
||||||
|
return fmt.Errorf("invalid IP address %#v", ipStrI.(string))
|
||||||
|
}
|
||||||
|
certReq.IPAddresses = append(certReq.IPAddresses, ip)
|
||||||
|
}
|
||||||
|
|
||||||
|
certReqBytes, err := x509.CreateCertificateRequest(rand.Reader, &certReq, key)
|
||||||
|
if err != nil {
|
||||||
|
fmt.Errorf("Error creating certificate request: %s", err)
|
||||||
|
}
|
||||||
|
certReqPem := string(pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE REQUEST", Bytes: certReqBytes}))
|
||||||
|
|
||||||
|
d.SetId(hashForState(string(certReqBytes)))
|
||||||
|
d.Set("cert_request_pem", certReqPem)
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func DeleteCertRequest(d *schema.ResourceData, meta interface{}) error {
|
||||||
|
d.SetId("")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func ReadCertRequest(d *schema.ResourceData, meta interface{}) error {
|
||||||
|
return nil
|
||||||
|
}
|
|
@ -0,0 +1,115 @@
|
||||||
|
package tls
|
||||||
|
|
||||||
|
import (
|
||||||
|
"crypto/x509"
|
||||||
|
"encoding/pem"
|
||||||
|
"fmt"
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
r "github.com/hashicorp/terraform/helper/resource"
|
||||||
|
"github.com/hashicorp/terraform/terraform"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestCertRequest(t *testing.T) {
|
||||||
|
r.Test(t, r.TestCase{
|
||||||
|
Providers: testProviders,
|
||||||
|
Steps: []r.TestStep{
|
||||||
|
r.TestStep{
|
||||||
|
Config: fmt.Sprintf(`
|
||||||
|
resource "tls_cert_request" "test" {
|
||||||
|
subject {
|
||||||
|
common_name = "example.com"
|
||||||
|
organization = "Example, Inc"
|
||||||
|
organizational_unit = "Department of Terraform Testing"
|
||||||
|
street_address = ["5879 Cotton Link"]
|
||||||
|
locality = "Pirate Harbor"
|
||||||
|
province = "CA"
|
||||||
|
country = "US"
|
||||||
|
postal_code = "95559-1227"
|
||||||
|
serial_number = "2"
|
||||||
|
}
|
||||||
|
|
||||||
|
dns_names = [
|
||||||
|
"example.com",
|
||||||
|
"example.net",
|
||||||
|
]
|
||||||
|
|
||||||
|
ip_addresses = [
|
||||||
|
"127.0.0.1",
|
||||||
|
"127.0.0.2",
|
||||||
|
]
|
||||||
|
|
||||||
|
key_algorithm = "RSA"
|
||||||
|
private_key_pem = <<EOT
|
||||||
|
%s
|
||||||
|
EOT
|
||||||
|
}
|
||||||
|
output "key_pem" {
|
||||||
|
value = "${tls_cert_request.test.cert_request_pem}"
|
||||||
|
}
|
||||||
|
`, testPrivateKey),
|
||||||
|
Check: func(s *terraform.State) error {
|
||||||
|
got := s.RootModule().Outputs["key_pem"]
|
||||||
|
if !strings.HasPrefix(got, "-----BEGIN CERTIFICATE REQUEST----") {
|
||||||
|
return fmt.Errorf("key is missing CSR PEM preamble")
|
||||||
|
}
|
||||||
|
block, _ := pem.Decode([]byte(got))
|
||||||
|
csr, err := x509.ParseCertificateRequest(block.Bytes)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("error parsing CSR: %s", err)
|
||||||
|
}
|
||||||
|
if expected, got := "2", csr.Subject.SerialNumber; got != expected {
|
||||||
|
return fmt.Errorf("incorrect subject serial number: expected %v, got %v", expected, got)
|
||||||
|
}
|
||||||
|
if expected, got := "example.com", csr.Subject.CommonName; got != expected {
|
||||||
|
return fmt.Errorf("incorrect subject common name: expected %v, got %v", expected, got)
|
||||||
|
}
|
||||||
|
if expected, got := "Example, Inc", csr.Subject.Organization[0]; got != expected {
|
||||||
|
return fmt.Errorf("incorrect subject organization: expected %v, got %v", expected, got)
|
||||||
|
}
|
||||||
|
if expected, got := "Department of Terraform Testing", csr.Subject.OrganizationalUnit[0]; got != expected {
|
||||||
|
return fmt.Errorf("incorrect subject organizational unit: expected %v, got %v", expected, got)
|
||||||
|
}
|
||||||
|
if expected, got := "5879 Cotton Link", csr.Subject.StreetAddress[0]; got != expected {
|
||||||
|
return fmt.Errorf("incorrect subject street address: expected %v, got %v", expected, got)
|
||||||
|
}
|
||||||
|
if expected, got := "Pirate Harbor", csr.Subject.Locality[0]; got != expected {
|
||||||
|
return fmt.Errorf("incorrect subject locality: expected %v, got %v", expected, got)
|
||||||
|
}
|
||||||
|
if expected, got := "CA", csr.Subject.Province[0]; got != expected {
|
||||||
|
return fmt.Errorf("incorrect subject province: expected %v, got %v", expected, got)
|
||||||
|
}
|
||||||
|
if expected, got := "US", csr.Subject.Country[0]; got != expected {
|
||||||
|
return fmt.Errorf("incorrect subject country: expected %v, got %v", expected, got)
|
||||||
|
}
|
||||||
|
if expected, got := "95559-1227", csr.Subject.PostalCode[0]; got != expected {
|
||||||
|
return fmt.Errorf("incorrect subject postal code: expected %v, got %v", expected, got)
|
||||||
|
}
|
||||||
|
|
||||||
|
if expected, got := 2, len(csr.DNSNames); got != expected {
|
||||||
|
return fmt.Errorf("incorrect number of DNS names: expected %v, got %v", expected, got)
|
||||||
|
}
|
||||||
|
if expected, got := "example.com", csr.DNSNames[0]; got != expected {
|
||||||
|
return fmt.Errorf("incorrect DNS name 0: expected %v, got %v", expected, got)
|
||||||
|
}
|
||||||
|
if expected, got := "example.net", csr.DNSNames[1]; got != expected {
|
||||||
|
return fmt.Errorf("incorrect DNS name 0: expected %v, got %v", expected, got)
|
||||||
|
}
|
||||||
|
|
||||||
|
if expected, got := 2, len(csr.IPAddresses); got != expected {
|
||||||
|
return fmt.Errorf("incorrect number of IP addresses: expected %v, got %v", expected, got)
|
||||||
|
}
|
||||||
|
if expected, got := "127.0.0.1", csr.IPAddresses[0].String(); got != expected {
|
||||||
|
return fmt.Errorf("incorrect IP address 0: expected %v, got %v", expected, got)
|
||||||
|
}
|
||||||
|
if expected, got := "127.0.0.2", csr.IPAddresses[1].String(); got != expected {
|
||||||
|
return fmt.Errorf("incorrect IP address 0: expected %v, got %v", expected, got)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
|
@ -0,0 +1,144 @@
|
||||||
|
package tls
|
||||||
|
|
||||||
|
import (
|
||||||
|
"crypto/ecdsa"
|
||||||
|
"crypto/elliptic"
|
||||||
|
"crypto/rand"
|
||||||
|
"crypto/rsa"
|
||||||
|
"crypto/x509"
|
||||||
|
"encoding/pem"
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"github.com/hashicorp/terraform/helper/schema"
|
||||||
|
)
|
||||||
|
|
||||||
|
type keyAlgo func(d *schema.ResourceData) (interface{}, error)
|
||||||
|
type keyParser func([]byte) (interface{}, error)
|
||||||
|
|
||||||
|
var keyAlgos map[string]keyAlgo = map[string]keyAlgo{
|
||||||
|
"RSA": func(d *schema.ResourceData) (interface{}, error) {
|
||||||
|
rsaBits := d.Get("rsa_bits").(int)
|
||||||
|
return rsa.GenerateKey(rand.Reader, rsaBits)
|
||||||
|
},
|
||||||
|
"ECDSA": func(d *schema.ResourceData) (interface{}, error) {
|
||||||
|
curve := d.Get("ecdsa_curve").(string)
|
||||||
|
switch curve {
|
||||||
|
case "P224":
|
||||||
|
return ecdsa.GenerateKey(elliptic.P224(), rand.Reader)
|
||||||
|
case "P256":
|
||||||
|
return ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
|
||||||
|
case "P384":
|
||||||
|
return ecdsa.GenerateKey(elliptic.P384(), rand.Reader)
|
||||||
|
case "P521":
|
||||||
|
return ecdsa.GenerateKey(elliptic.P521(), rand.Reader)
|
||||||
|
default:
|
||||||
|
return nil, fmt.Errorf("invalid ecdsa_curve; must be P224, P256, P384 or P521")
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
var keyParsers map[string]keyParser = map[string]keyParser{
|
||||||
|
"RSA": func(der []byte) (interface{}, error) {
|
||||||
|
return x509.ParsePKCS1PrivateKey(der)
|
||||||
|
},
|
||||||
|
"ECDSA": func(der []byte) (interface{}, error) {
|
||||||
|
return x509.ParseECPrivateKey(der)
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
func resourcePrivateKey() *schema.Resource {
|
||||||
|
return &schema.Resource{
|
||||||
|
Create: CreatePrivateKey,
|
||||||
|
Delete: DeletePrivateKey,
|
||||||
|
Read: ReadPrivateKey,
|
||||||
|
|
||||||
|
Schema: map[string]*schema.Schema{
|
||||||
|
"algorithm": &schema.Schema{
|
||||||
|
Type: schema.TypeString,
|
||||||
|
Required: true,
|
||||||
|
Description: "Name of the algorithm to use to generate the private key",
|
||||||
|
ForceNew: true,
|
||||||
|
},
|
||||||
|
|
||||||
|
"rsa_bits": &schema.Schema{
|
||||||
|
Type: schema.TypeInt,
|
||||||
|
Optional: true,
|
||||||
|
Description: "Number of bits to use when generating an RSA key",
|
||||||
|
ForceNew: true,
|
||||||
|
Default: 2048,
|
||||||
|
},
|
||||||
|
|
||||||
|
"ecdsa_curve": &schema.Schema{
|
||||||
|
Type: schema.TypeString,
|
||||||
|
Optional: true,
|
||||||
|
Description: "ECDSA curve to use when generating a key",
|
||||||
|
ForceNew: true,
|
||||||
|
Default: "P224",
|
||||||
|
},
|
||||||
|
|
||||||
|
"private_key_pem": &schema.Schema{
|
||||||
|
Type: schema.TypeString,
|
||||||
|
Computed: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func CreatePrivateKey(d *schema.ResourceData, meta interface{}) error {
|
||||||
|
keyAlgoName := d.Get("algorithm").(string)
|
||||||
|
var keyFunc keyAlgo
|
||||||
|
var ok bool
|
||||||
|
if keyFunc, ok = keyAlgos[keyAlgoName]; !ok {
|
||||||
|
return fmt.Errorf("invalid key_algorithm %#v", keyAlgoName)
|
||||||
|
}
|
||||||
|
|
||||||
|
key, err := keyFunc(d)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
var keyPemBlock *pem.Block
|
||||||
|
switch k := key.(type) {
|
||||||
|
case *rsa.PrivateKey:
|
||||||
|
keyPemBlock = &pem.Block{Type: "RSA PRIVATE KEY", Bytes: x509.MarshalPKCS1PrivateKey(k)}
|
||||||
|
case *ecdsa.PrivateKey:
|
||||||
|
b, err := x509.MarshalECPrivateKey(k)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("error encoding key to PEM: %s", err)
|
||||||
|
}
|
||||||
|
keyPemBlock = &pem.Block{Type: "EC PRIVATE KEY", Bytes: b}
|
||||||
|
default:
|
||||||
|
return fmt.Errorf("unsupported private key type")
|
||||||
|
}
|
||||||
|
keyPem := string(pem.EncodeToMemory(keyPemBlock))
|
||||||
|
|
||||||
|
pubKeyBytes, err := x509.MarshalPKIXPublicKey(publicKey(key))
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to marshal public key: %s", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
d.SetId(hashForState(string((pubKeyBytes))))
|
||||||
|
d.Set("private_key_pem", keyPem)
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func DeletePrivateKey(d *schema.ResourceData, meta interface{}) error {
|
||||||
|
d.SetId("")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func ReadPrivateKey(d *schema.ResourceData, meta interface{}) error {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func publicKey(priv interface{}) interface{} {
|
||||||
|
switch k := priv.(type) {
|
||||||
|
case *rsa.PrivateKey:
|
||||||
|
return &k.PublicKey
|
||||||
|
case *ecdsa.PrivateKey:
|
||||||
|
return &k.PublicKey
|
||||||
|
default:
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,84 @@
|
||||||
|
package tls
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
r "github.com/hashicorp/terraform/helper/resource"
|
||||||
|
"github.com/hashicorp/terraform/terraform"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestPrivateKeyRSA(t *testing.T) {
|
||||||
|
r.Test(t, r.TestCase{
|
||||||
|
Providers: testProviders,
|
||||||
|
Steps: []r.TestStep{
|
||||||
|
r.TestStep{
|
||||||
|
Config: `
|
||||||
|
resource "tls_private_key" "test" {
|
||||||
|
algorithm = "RSA"
|
||||||
|
}
|
||||||
|
output "key_pem" {
|
||||||
|
value = "${tls_private_key.test.private_key_pem}"
|
||||||
|
}
|
||||||
|
`,
|
||||||
|
Check: func(s *terraform.State) error {
|
||||||
|
got := s.RootModule().Outputs["key_pem"]
|
||||||
|
if !strings.HasPrefix(got, "-----BEGIN RSA PRIVATE KEY----") {
|
||||||
|
return fmt.Errorf("key is missing RSA key PEM preamble")
|
||||||
|
}
|
||||||
|
if len(got) > 1700 {
|
||||||
|
return fmt.Errorf("key PEM looks too long for a 2048-bit key (got %v characters)", len(got))
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
},
|
||||||
|
},
|
||||||
|
r.TestStep{
|
||||||
|
Config: `
|
||||||
|
resource "tls_private_key" "test" {
|
||||||
|
algorithm = "RSA"
|
||||||
|
rsa_bits = 4096
|
||||||
|
}
|
||||||
|
output "key_pem" {
|
||||||
|
value = "${tls_private_key.test.private_key_pem}"
|
||||||
|
}
|
||||||
|
`,
|
||||||
|
Check: func(s *terraform.State) error {
|
||||||
|
got := s.RootModule().Outputs["key_pem"]
|
||||||
|
if !strings.HasPrefix(got, "-----BEGIN RSA PRIVATE KEY----") {
|
||||||
|
return fmt.Errorf("key is missing RSA key PEM preamble")
|
||||||
|
}
|
||||||
|
if len(got) < 1700 {
|
||||||
|
return fmt.Errorf("key PEM looks too short for a 4096-bit key (got %v characters)", len(got))
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestPrivateKeyECDSA(t *testing.T) {
|
||||||
|
r.Test(t, r.TestCase{
|
||||||
|
Providers: testProviders,
|
||||||
|
Steps: []r.TestStep{
|
||||||
|
r.TestStep{
|
||||||
|
Config: `
|
||||||
|
resource "tls_private_key" "test" {
|
||||||
|
algorithm = "ECDSA"
|
||||||
|
}
|
||||||
|
output "key_pem" {
|
||||||
|
value = "${tls_private_key.test.private_key_pem}"
|
||||||
|
}
|
||||||
|
`,
|
||||||
|
Check: func(s *terraform.State) error {
|
||||||
|
got := s.RootModule().Outputs["key_pem"]
|
||||||
|
if !strings.HasPrefix(got, "-----BEGIN EC PRIVATE KEY----") {
|
||||||
|
return fmt.Errorf("Key is missing EC key PEM preamble")
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
|
@ -0,0 +1,265 @@
|
||||||
|
package tls
|
||||||
|
|
||||||
|
import (
|
||||||
|
"crypto/rand"
|
||||||
|
"crypto/x509"
|
||||||
|
"encoding/pem"
|
||||||
|
"fmt"
|
||||||
|
"math/big"
|
||||||
|
"net"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/hashicorp/terraform/helper/schema"
|
||||||
|
)
|
||||||
|
|
||||||
|
var keyUsages map[string]x509.KeyUsage = map[string]x509.KeyUsage{
|
||||||
|
"digital_signature": x509.KeyUsageDigitalSignature,
|
||||||
|
"content_commitment": x509.KeyUsageContentCommitment,
|
||||||
|
"key_encipherment": x509.KeyUsageKeyEncipherment,
|
||||||
|
"data_encipherment": x509.KeyUsageDataEncipherment,
|
||||||
|
"key_agreement": x509.KeyUsageKeyAgreement,
|
||||||
|
"cert_signing": x509.KeyUsageCertSign,
|
||||||
|
"crl_signing": x509.KeyUsageCRLSign,
|
||||||
|
"encipher_only": x509.KeyUsageEncipherOnly,
|
||||||
|
"decipher_only": x509.KeyUsageDecipherOnly,
|
||||||
|
}
|
||||||
|
|
||||||
|
var extKeyUsages map[string]x509.ExtKeyUsage = map[string]x509.ExtKeyUsage{
|
||||||
|
"any_extended": x509.ExtKeyUsageAny,
|
||||||
|
"server_auth": x509.ExtKeyUsageServerAuth,
|
||||||
|
"client_auth": x509.ExtKeyUsageClientAuth,
|
||||||
|
"code_signing": x509.ExtKeyUsageCodeSigning,
|
||||||
|
"email_protection": x509.ExtKeyUsageEmailProtection,
|
||||||
|
"ipsec_end_system": x509.ExtKeyUsageIPSECEndSystem,
|
||||||
|
"ipsec_tunnel": x509.ExtKeyUsageIPSECTunnel,
|
||||||
|
"ipsec_user": x509.ExtKeyUsageIPSECUser,
|
||||||
|
"timestamping": x509.ExtKeyUsageTimeStamping,
|
||||||
|
"ocsp_signing": x509.ExtKeyUsageOCSPSigning,
|
||||||
|
"microsoft_server_gated_crypto": x509.ExtKeyUsageMicrosoftServerGatedCrypto,
|
||||||
|
"netscape_server_gated_crypto": x509.ExtKeyUsageNetscapeServerGatedCrypto,
|
||||||
|
}
|
||||||
|
|
||||||
|
func resourceSelfSignedCert() *schema.Resource {
|
||||||
|
return &schema.Resource{
|
||||||
|
Create: CreateSelfSignedCert,
|
||||||
|
Delete: DeleteSelfSignedCert,
|
||||||
|
Read: ReadSelfSignedCert,
|
||||||
|
|
||||||
|
Schema: map[string]*schema.Schema{
|
||||||
|
|
||||||
|
"dns_names": &schema.Schema{
|
||||||
|
Type: schema.TypeList,
|
||||||
|
Optional: true,
|
||||||
|
Description: "List of DNS names to use as subjects of the certificate",
|
||||||
|
ForceNew: true,
|
||||||
|
Elem: &schema.Schema{
|
||||||
|
Type: schema.TypeString,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
"ip_addresses": &schema.Schema{
|
||||||
|
Type: schema.TypeList,
|
||||||
|
Optional: true,
|
||||||
|
Description: "List of IP addresses to use as subjects of the certificate",
|
||||||
|
ForceNew: true,
|
||||||
|
Elem: &schema.Schema{
|
||||||
|
Type: schema.TypeString,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
"validity_period_hours": &schema.Schema{
|
||||||
|
Type: schema.TypeInt,
|
||||||
|
Required: true,
|
||||||
|
Description: "Number of hours that the certificate will remain valid for",
|
||||||
|
ForceNew: true,
|
||||||
|
},
|
||||||
|
|
||||||
|
"early_renewal_hours": &schema.Schema{
|
||||||
|
Type: schema.TypeInt,
|
||||||
|
Optional: true,
|
||||||
|
Default: 0,
|
||||||
|
Description: "Number of hours before the certificates expiry when a new certificate will be generated",
|
||||||
|
ForceNew: true,
|
||||||
|
},
|
||||||
|
|
||||||
|
"is_ca_certificate": &schema.Schema{
|
||||||
|
Type: schema.TypeBool,
|
||||||
|
Optional: true,
|
||||||
|
Description: "Whether the generated certificate will be usable as a CA certificate",
|
||||||
|
ForceNew: true,
|
||||||
|
},
|
||||||
|
|
||||||
|
"allowed_uses": &schema.Schema{
|
||||||
|
Type: schema.TypeList,
|
||||||
|
Required: true,
|
||||||
|
Description: "Uses that are allowed for the certificate",
|
||||||
|
ForceNew: true,
|
||||||
|
Elem: &schema.Schema{
|
||||||
|
Type: schema.TypeString,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
"key_algorithm": &schema.Schema{
|
||||||
|
Type: schema.TypeString,
|
||||||
|
Required: true,
|
||||||
|
Description: "Name of the algorithm to use to generate the certificate's private key",
|
||||||
|
ForceNew: true,
|
||||||
|
},
|
||||||
|
|
||||||
|
"private_key_pem": &schema.Schema{
|
||||||
|
Type: schema.TypeString,
|
||||||
|
Required: true,
|
||||||
|
Description: "PEM-encoded private key that the certificate will belong to",
|
||||||
|
ForceNew: true,
|
||||||
|
StateFunc: func(v interface{}) string {
|
||||||
|
return hashForState(v.(string))
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
"subject": &schema.Schema{
|
||||||
|
Type: schema.TypeList,
|
||||||
|
Required: true,
|
||||||
|
Elem: nameSchema,
|
||||||
|
ForceNew: true,
|
||||||
|
},
|
||||||
|
|
||||||
|
"cert_pem": &schema.Schema{
|
||||||
|
Type: schema.TypeString,
|
||||||
|
Computed: true,
|
||||||
|
},
|
||||||
|
|
||||||
|
"validity_start_time": &schema.Schema{
|
||||||
|
Type: schema.TypeString,
|
||||||
|
Computed: true,
|
||||||
|
},
|
||||||
|
|
||||||
|
"validity_end_time": &schema.Schema{
|
||||||
|
Type: schema.TypeString,
|
||||||
|
Computed: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func CreateSelfSignedCert(d *schema.ResourceData, meta interface{}) error {
|
||||||
|
keyAlgoName := d.Get("key_algorithm").(string)
|
||||||
|
var keyFunc keyParser
|
||||||
|
var ok bool
|
||||||
|
if keyFunc, ok = keyParsers[keyAlgoName]; !ok {
|
||||||
|
return fmt.Errorf("invalid key_algorithm %#v", keyAlgoName)
|
||||||
|
}
|
||||||
|
keyBlock, _ := pem.Decode([]byte(d.Get("private_key_pem").(string)))
|
||||||
|
if keyBlock == nil {
|
||||||
|
return fmt.Errorf("no PEM block found in private_key_pem")
|
||||||
|
}
|
||||||
|
key, err := keyFunc(keyBlock.Bytes)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to decode private_key_pem: %s", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
notBefore := time.Now()
|
||||||
|
notAfter := notBefore.Add(time.Duration(d.Get("validity_period_hours").(int)) * time.Hour)
|
||||||
|
|
||||||
|
serialNumberLimit := new(big.Int).Lsh(big.NewInt(1), 128)
|
||||||
|
serialNumber, err := rand.Int(rand.Reader, serialNumberLimit)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to generate serial number: %s", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
subjectConfs := d.Get("subject").([]interface{})
|
||||||
|
if len(subjectConfs) != 1 {
|
||||||
|
return fmt.Errorf("must have exactly one 'subject' block")
|
||||||
|
}
|
||||||
|
subjectConf := subjectConfs[0].(map[string]interface{})
|
||||||
|
subject, err := nameFromResourceData(subjectConf)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("invalid subject block: %s", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
cert := x509.Certificate{
|
||||||
|
SerialNumber: serialNumber,
|
||||||
|
Subject: *subject,
|
||||||
|
NotBefore: notBefore,
|
||||||
|
NotAfter: notAfter,
|
||||||
|
BasicConstraintsValid: true,
|
||||||
|
}
|
||||||
|
|
||||||
|
keyUsesI := d.Get("allowed_uses").([]interface{})
|
||||||
|
for _, keyUseI := range keyUsesI {
|
||||||
|
keyUse := keyUseI.(string)
|
||||||
|
if usage, ok := keyUsages[keyUse]; ok {
|
||||||
|
cert.KeyUsage |= usage
|
||||||
|
}
|
||||||
|
if usage, ok := extKeyUsages[keyUse]; ok {
|
||||||
|
cert.ExtKeyUsage = append(cert.ExtKeyUsage, usage)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
dnsNamesI := d.Get("dns_names").([]interface{})
|
||||||
|
for _, nameI := range dnsNamesI {
|
||||||
|
cert.DNSNames = append(cert.DNSNames, nameI.(string))
|
||||||
|
}
|
||||||
|
ipAddressesI := d.Get("ip_addresses").([]interface{})
|
||||||
|
for _, ipStrI := range ipAddressesI {
|
||||||
|
ip := net.ParseIP(ipStrI.(string))
|
||||||
|
if ip == nil {
|
||||||
|
return fmt.Errorf("invalid IP address %#v", ipStrI.(string))
|
||||||
|
}
|
||||||
|
cert.IPAddresses = append(cert.IPAddresses, ip)
|
||||||
|
}
|
||||||
|
|
||||||
|
if d.Get("is_ca_certificate").(bool) {
|
||||||
|
cert.IsCA = true
|
||||||
|
}
|
||||||
|
|
||||||
|
certBytes, err := x509.CreateCertificate(rand.Reader, &cert, &cert, publicKey(key), key)
|
||||||
|
if err != nil {
|
||||||
|
fmt.Errorf("Error creating certificate: %s", err)
|
||||||
|
}
|
||||||
|
certPem := string(pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: certBytes}))
|
||||||
|
|
||||||
|
validFromBytes, err := notBefore.MarshalText()
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("error serializing validity_start_time: %s", err)
|
||||||
|
}
|
||||||
|
validToBytes, err := notAfter.MarshalText()
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("error serializing validity_end_time: %s", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
d.SetId(serialNumber.String())
|
||||||
|
d.Set("cert_pem", certPem)
|
||||||
|
d.Set("validity_start_time", string(validFromBytes))
|
||||||
|
d.Set("validity_end_time", string(validToBytes))
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func DeleteSelfSignedCert(d *schema.ResourceData, meta interface{}) error {
|
||||||
|
d.SetId("")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func ReadSelfSignedCert(d *schema.ResourceData, meta interface{}) error {
|
||||||
|
|
||||||
|
endTimeStr := d.Get("validity_end_time").(string)
|
||||||
|
endTime := time.Now()
|
||||||
|
err := endTime.UnmarshalText([]byte(endTimeStr))
|
||||||
|
if err != nil {
|
||||||
|
// If end time is invalid then we'll just throw away the whole
|
||||||
|
// thing so we can generate a new one.
|
||||||
|
d.SetId("")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
earlyRenewalPeriod := time.Duration(-d.Get("early_renewal_hours").(int)) * time.Hour
|
||||||
|
endTime = endTime.Add(earlyRenewalPeriod)
|
||||||
|
|
||||||
|
if time.Now().After(endTime) {
|
||||||
|
// Treat an expired certificate as not existing, so we'll generate
|
||||||
|
// a new one with the next plan.
|
||||||
|
d.SetId("")
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
|
@ -0,0 +1,152 @@
|
||||||
|
package tls
|
||||||
|
|
||||||
|
import (
|
||||||
|
"crypto/x509"
|
||||||
|
"encoding/pem"
|
||||||
|
"fmt"
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
r "github.com/hashicorp/terraform/helper/resource"
|
||||||
|
"github.com/hashicorp/terraform/terraform"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestSelfSignedCert(t *testing.T) {
|
||||||
|
r.Test(t, r.TestCase{
|
||||||
|
Providers: testProviders,
|
||||||
|
Steps: []r.TestStep{
|
||||||
|
r.TestStep{
|
||||||
|
Config: fmt.Sprintf(`
|
||||||
|
resource "tls_self_signed_cert" "test" {
|
||||||
|
subject {
|
||||||
|
common_name = "example.com"
|
||||||
|
organization = "Example, Inc"
|
||||||
|
organizational_unit = "Department of Terraform Testing"
|
||||||
|
street_address = ["5879 Cotton Link"]
|
||||||
|
locality = "Pirate Harbor"
|
||||||
|
province = "CA"
|
||||||
|
country = "US"
|
||||||
|
postal_code = "95559-1227"
|
||||||
|
serial_number = "2"
|
||||||
|
}
|
||||||
|
|
||||||
|
dns_names = [
|
||||||
|
"example.com",
|
||||||
|
"example.net",
|
||||||
|
]
|
||||||
|
|
||||||
|
ip_addresses = [
|
||||||
|
"127.0.0.1",
|
||||||
|
"127.0.0.2",
|
||||||
|
]
|
||||||
|
|
||||||
|
validity_period_hours = 1
|
||||||
|
|
||||||
|
allowed_uses = [
|
||||||
|
"key_encipherment",
|
||||||
|
"digital_signature",
|
||||||
|
"server_auth",
|
||||||
|
"client_auth",
|
||||||
|
]
|
||||||
|
|
||||||
|
key_algorithm = "RSA"
|
||||||
|
private_key_pem = <<EOT
|
||||||
|
%s
|
||||||
|
EOT
|
||||||
|
}
|
||||||
|
output "key_pem" {
|
||||||
|
value = "${tls_self_signed_cert.test.cert_pem}"
|
||||||
|
}
|
||||||
|
`, testPrivateKey),
|
||||||
|
Check: func(s *terraform.State) error {
|
||||||
|
got := s.RootModule().Outputs["key_pem"]
|
||||||
|
if !strings.HasPrefix(got, "-----BEGIN CERTIFICATE----") {
|
||||||
|
return fmt.Errorf("key is missing cert PEM preamble")
|
||||||
|
}
|
||||||
|
block, _ := pem.Decode([]byte(got))
|
||||||
|
cert, err := x509.ParseCertificate(block.Bytes)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("error parsing cert: %s", err)
|
||||||
|
}
|
||||||
|
if expected, got := "2", cert.Subject.SerialNumber; got != expected {
|
||||||
|
return fmt.Errorf("incorrect subject serial number: expected %v, got %v", expected, got)
|
||||||
|
}
|
||||||
|
if expected, got := "example.com", cert.Subject.CommonName; got != expected {
|
||||||
|
return fmt.Errorf("incorrect subject common name: expected %v, got %v", expected, got)
|
||||||
|
}
|
||||||
|
if expected, got := "Example, Inc", cert.Subject.Organization[0]; got != expected {
|
||||||
|
return fmt.Errorf("incorrect subject organization: expected %v, got %v", expected, got)
|
||||||
|
}
|
||||||
|
if expected, got := "Department of Terraform Testing", cert.Subject.OrganizationalUnit[0]; got != expected {
|
||||||
|
return fmt.Errorf("incorrect subject organizational unit: expected %v, got %v", expected, got)
|
||||||
|
}
|
||||||
|
if expected, got := "5879 Cotton Link", cert.Subject.StreetAddress[0]; got != expected {
|
||||||
|
return fmt.Errorf("incorrect subject street address: expected %v, got %v", expected, got)
|
||||||
|
}
|
||||||
|
if expected, got := "Pirate Harbor", cert.Subject.Locality[0]; got != expected {
|
||||||
|
return fmt.Errorf("incorrect subject locality: expected %v, got %v", expected, got)
|
||||||
|
}
|
||||||
|
if expected, got := "CA", cert.Subject.Province[0]; got != expected {
|
||||||
|
return fmt.Errorf("incorrect subject province: expected %v, got %v", expected, got)
|
||||||
|
}
|
||||||
|
if expected, got := "US", cert.Subject.Country[0]; got != expected {
|
||||||
|
return fmt.Errorf("incorrect subject country: expected %v, got %v", expected, got)
|
||||||
|
}
|
||||||
|
if expected, got := "95559-1227", cert.Subject.PostalCode[0]; got != expected {
|
||||||
|
return fmt.Errorf("incorrect subject postal code: expected %v, got %v", expected, got)
|
||||||
|
}
|
||||||
|
|
||||||
|
if expected, got := 2, len(cert.DNSNames); got != expected {
|
||||||
|
return fmt.Errorf("incorrect number of DNS names: expected %v, got %v", expected, got)
|
||||||
|
}
|
||||||
|
if expected, got := "example.com", cert.DNSNames[0]; got != expected {
|
||||||
|
return fmt.Errorf("incorrect DNS name 0: expected %v, got %v", expected, got)
|
||||||
|
}
|
||||||
|
if expected, got := "example.net", cert.DNSNames[1]; got != expected {
|
||||||
|
return fmt.Errorf("incorrect DNS name 0: expected %v, got %v", expected, got)
|
||||||
|
}
|
||||||
|
|
||||||
|
if expected, got := 2, len(cert.IPAddresses); got != expected {
|
||||||
|
return fmt.Errorf("incorrect number of IP addresses: expected %v, got %v", expected, got)
|
||||||
|
}
|
||||||
|
if expected, got := "127.0.0.1", cert.IPAddresses[0].String(); got != expected {
|
||||||
|
return fmt.Errorf("incorrect IP address 0: expected %v, got %v", expected, got)
|
||||||
|
}
|
||||||
|
if expected, got := "127.0.0.2", cert.IPAddresses[1].String(); got != expected {
|
||||||
|
return fmt.Errorf("incorrect IP address 0: expected %v, got %v", expected, got)
|
||||||
|
}
|
||||||
|
|
||||||
|
if expected, got := 2, len(cert.ExtKeyUsage); got != expected {
|
||||||
|
return fmt.Errorf("incorrect number of ExtKeyUsage: expected %v, got %v", expected, got)
|
||||||
|
}
|
||||||
|
if expected, got := x509.ExtKeyUsageServerAuth, cert.ExtKeyUsage[0]; got != expected {
|
||||||
|
return fmt.Errorf("incorrect ExtKeyUsage[0]: expected %v, got %v", expected, got)
|
||||||
|
}
|
||||||
|
if expected, got := x509.ExtKeyUsageClientAuth, cert.ExtKeyUsage[1]; got != expected {
|
||||||
|
return fmt.Errorf("incorrect ExtKeyUsage[1]: expected %v, got %v", expected, got)
|
||||||
|
}
|
||||||
|
|
||||||
|
if expected, got := x509.KeyUsageKeyEncipherment|x509.KeyUsageDigitalSignature, cert.KeyUsage; got != expected {
|
||||||
|
return fmt.Errorf("incorrect KeyUsage: expected %v, got %v", expected, got)
|
||||||
|
}
|
||||||
|
|
||||||
|
// This time checking is a bit sloppy to avoid inconsistent test results
|
||||||
|
// depending on the power of the machine running the tests.
|
||||||
|
now := time.Now()
|
||||||
|
if cert.NotBefore.After(now) {
|
||||||
|
return fmt.Errorf("certificate validity begins in the future")
|
||||||
|
}
|
||||||
|
if now.Sub(cert.NotBefore) > (2 * time.Minute) {
|
||||||
|
return fmt.Errorf("certificate validity begins more than two minutes in the past")
|
||||||
|
}
|
||||||
|
if cert.NotAfter.Sub(cert.NotBefore) != time.Hour {
|
||||||
|
return fmt.Errorf("certificate validity is not one hour")
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
|
@ -23,6 +23,7 @@ body.layout-openstack,
|
||||||
body.layout-packet,
|
body.layout-packet,
|
||||||
body.layout-rundeck,
|
body.layout-rundeck,
|
||||||
body.layout-template,
|
body.layout-template,
|
||||||
|
body.layout-tls,
|
||||||
body.layout-vsphere,
|
body.layout-vsphere,
|
||||||
body.layout-docs,
|
body.layout-docs,
|
||||||
body.layout-downloads,
|
body.layout-downloads,
|
||||||
|
|
|
@ -0,0 +1,72 @@
|
||||||
|
---
|
||||||
|
layout: "tls"
|
||||||
|
page_title: "Provider: TLS"
|
||||||
|
sidebar_current: "docs-tls-index"
|
||||||
|
description: |-
|
||||||
|
The TLS provider provides utilities for working with Transport Layer Security keys and certificates.
|
||||||
|
---
|
||||||
|
|
||||||
|
# TLS Provider
|
||||||
|
|
||||||
|
The TLS provider provides utilities for working with *Transport Layer Security*
|
||||||
|
keys and certificates. It provides resources that
|
||||||
|
allow private keys, certificates and certficate requests to be
|
||||||
|
created as part of a Terraform deployment.
|
||||||
|
|
||||||
|
Another name for Transport Layer Security is *Secure Sockets Layer*,
|
||||||
|
or SSL. TLS and SSL are equivalent when considering the resources
|
||||||
|
managed by this provider.
|
||||||
|
|
||||||
|
This provider is not particularly useful on its own, but it can be
|
||||||
|
used to create certificates and credentials that can then be used
|
||||||
|
with other providers when creating resources that expose TLS
|
||||||
|
services or that themselves provision TLS certificates.
|
||||||
|
|
||||||
|
Use the navigation to the left to read about the available resources.
|
||||||
|
|
||||||
|
## Example Usage
|
||||||
|
|
||||||
|
```
|
||||||
|
## This example create a self-signed certificate for a development
|
||||||
|
## environment.
|
||||||
|
## THIS IS NOT RECOMMENDED FOR PRODUCTION SERVICES.
|
||||||
|
## See the detailed documentation of each resource for further
|
||||||
|
## security considerations and other practical tradeoffs.
|
||||||
|
|
||||||
|
resource "tls_private_key" "example" {
|
||||||
|
algorithm = "ECDSA"
|
||||||
|
}
|
||||||
|
|
||||||
|
resource "tls_self_signed_cert" "example" {
|
||||||
|
key_algorithm = "${tls_private_key.example.algorithm}"
|
||||||
|
private_key_pem = "${tls_private_key.example.private_key_pem}"
|
||||||
|
|
||||||
|
# Certificate expires after 12 hours.
|
||||||
|
validity_period_hours = 12
|
||||||
|
|
||||||
|
# Generate a new certificate if Terraform is run within three
|
||||||
|
# hours of the certificate's expiration time.
|
||||||
|
early_renewal_hours = 3
|
||||||
|
|
||||||
|
# Reasonable set of uses for a server SSL certificate.
|
||||||
|
allowed_uses = [
|
||||||
|
"key_encipherment",
|
||||||
|
"digital_signature",
|
||||||
|
"server_auth",
|
||||||
|
]
|
||||||
|
|
||||||
|
dns_names = ["example.com", "example.net"]
|
||||||
|
|
||||||
|
subject {
|
||||||
|
common_name = "example.com"
|
||||||
|
organization = "ACME Examples, Inc"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
# For example, this can be used to populate an AWS IAM server certificate.
|
||||||
|
resource "aws_iam_server_certificate" "example" {
|
||||||
|
name = "example_self_signed_cert"
|
||||||
|
certificate_body = "${tls_self_signed_cert.example.cert_pem}"
|
||||||
|
private_key = "${tls_private_key.example.private_key_pem}"
|
||||||
|
}
|
||||||
|
```
|
|
@ -0,0 +1,78 @@
|
||||||
|
---
|
||||||
|
layout: "tls"
|
||||||
|
page_title: "TLS: tls_cert_request"
|
||||||
|
sidebar_current: "docs-tls-resourse-cert-request"
|
||||||
|
description: |-
|
||||||
|
Creates a PEM-encoded certificate request.
|
||||||
|
---
|
||||||
|
|
||||||
|
# tls\_cert\_request
|
||||||
|
|
||||||
|
Generates a *Certificate Signing Request* (CSR) in PEM format, which is the
|
||||||
|
typical format used to request a certificate from a certificate authority.
|
||||||
|
|
||||||
|
This resource is intended to be used in conjunction with a Terraform provider
|
||||||
|
for a particular certificate authority in order to provision a new certificate.
|
||||||
|
This is a *logical resource*, so it contributes only to the current Terraform
|
||||||
|
state and does not create any external managed resources.
|
||||||
|
|
||||||
|
## Example Usage
|
||||||
|
|
||||||
|
```
|
||||||
|
resource "tls_cert_request" "example" {
|
||||||
|
key_algorithm = "ECDSA"
|
||||||
|
private_key_pem = "${file(\"private_key.pem\")}"
|
||||||
|
|
||||||
|
subject {
|
||||||
|
common_name = "example.com"
|
||||||
|
organization = "ACME Examples, Inc"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Argument Reference
|
||||||
|
|
||||||
|
The following arguments are supported:
|
||||||
|
|
||||||
|
* `key_algorithm` - (Required) The name of the algorithm for the key provided
|
||||||
|
in `private_key_pem`.
|
||||||
|
|
||||||
|
* `private_key_pem` - (Required) PEM-encoded private key data. This can be
|
||||||
|
read from a separate file using the ``file`` interpolation function. Only
|
||||||
|
an irreversable secure hash of the private key will be stored in the Terraform
|
||||||
|
state.
|
||||||
|
|
||||||
|
* `subject` - (Required) The subject for which a certificate is being requested. This is
|
||||||
|
a nested configuration block whose structure is described below.
|
||||||
|
|
||||||
|
* `dns_names` - (Optional) List of DNS names for which a certificate is being requested.
|
||||||
|
|
||||||
|
* `ip_addresses` - (Optional) List of IP addresses for which a certificate is being requested.
|
||||||
|
|
||||||
|
The nested `subject` block accepts the following arguments, all optional, with their meaning
|
||||||
|
corresponding to the similarly-named attributes defined in
|
||||||
|
[RFC5290](https://tools.ietf.org/html/rfc5280#section-4.1.2.4):
|
||||||
|
|
||||||
|
* `common_name` (string)
|
||||||
|
|
||||||
|
* `organization` (string)
|
||||||
|
|
||||||
|
* `organizational_unit` (string)
|
||||||
|
|
||||||
|
* `street_address` (list of strings)
|
||||||
|
|
||||||
|
* `locality` (string)
|
||||||
|
|
||||||
|
* `province` (string)
|
||||||
|
|
||||||
|
* `country` (string)
|
||||||
|
|
||||||
|
* `postal_code` (string)
|
||||||
|
|
||||||
|
* `serial_number` (string)
|
||||||
|
|
||||||
|
## Attributes Reference
|
||||||
|
|
||||||
|
The following attributes are exported:
|
||||||
|
|
||||||
|
* `cert_request_pem` - The certificate request data in PEM format.
|
|
@ -0,0 +1,66 @@
|
||||||
|
---
|
||||||
|
layout: "tls"
|
||||||
|
page_title: "TLS: tls_private_key"
|
||||||
|
sidebar_current: "docs-tls-resourse-private-key"
|
||||||
|
description: |-
|
||||||
|
Creates a PEM-encoded private key.
|
||||||
|
---
|
||||||
|
|
||||||
|
# tls\_private\_key
|
||||||
|
|
||||||
|
Generates a secure private key and encodes it as PEM. This resource is
|
||||||
|
primarily intended for easily bootstrapping throwaway development
|
||||||
|
environments.
|
||||||
|
|
||||||
|
~> **Important Security Notice** The private key generated by this resource will
|
||||||
|
be stored *unencrypted* in your Terraform state file. **Use of this resource
|
||||||
|
for production deployments is *not* recommended**. Instead, generate
|
||||||
|
a private key file outside of Terraform and distribute it securely
|
||||||
|
to the system where Terraform will be run.
|
||||||
|
|
||||||
|
This is a *logical resource*, so it contributes only to the current Terraform
|
||||||
|
state and does not create any external managed resources.
|
||||||
|
|
||||||
|
## Example Usage
|
||||||
|
|
||||||
|
```
|
||||||
|
resource "tls_private_key" "example" {
|
||||||
|
algorithm = "ECDSA"
|
||||||
|
ecdsa_curve = "P384"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Argument Reference
|
||||||
|
|
||||||
|
The following arguments are supported:
|
||||||
|
|
||||||
|
* `algorithm` - (Required) The name of the algorithm to use for
|
||||||
|
the key. Currently-supported values are "RSA" and "ECDSA".
|
||||||
|
|
||||||
|
* `rsa_bits` - (Optional) When `algorithm` is "RSA", the size of the generated
|
||||||
|
RSA key in bits. Defaults to 2048.
|
||||||
|
|
||||||
|
* `ecdsa_curve` - (Optional) When `algorithm` is "ECDSA", the name of the elliptic
|
||||||
|
curve to use. May be any one of "P224", "P256", "P384" or "P521", with "P224" as the
|
||||||
|
default.
|
||||||
|
|
||||||
|
## Attributes Reference
|
||||||
|
|
||||||
|
The following attributes are exported:
|
||||||
|
|
||||||
|
* `algorithm` - The algorithm that was selected for the key.
|
||||||
|
* `private_key_pem` - The private key data in PEM format.
|
||||||
|
|
||||||
|
## Generating a New Key
|
||||||
|
|
||||||
|
Since a private key is a logical resource that lives only in the Terraform state,
|
||||||
|
it will persist until it is explicitly destroyed by the user.
|
||||||
|
|
||||||
|
In order to force the generation of a new key within an existing state, the
|
||||||
|
private key instance can be "tainted":
|
||||||
|
|
||||||
|
```
|
||||||
|
terraform taint tls_private_key.example
|
||||||
|
```
|
||||||
|
|
||||||
|
A new key will then be generated on the next ``terraform apply``.
|
|
@ -0,0 +1,137 @@
|
||||||
|
---
|
||||||
|
layout: "tls"
|
||||||
|
page_title: "TLS: tls_self_signed_cert"
|
||||||
|
sidebar_current: "docs-tls-resourse-self-signed-cert"
|
||||||
|
description: |-
|
||||||
|
Creates a self-signed TLS certificate in PEM format.
|
||||||
|
---
|
||||||
|
|
||||||
|
# tls\_self\_signed\_cert
|
||||||
|
|
||||||
|
Generates a *self-signed* TLS certificate in PEM format, which is the typical
|
||||||
|
format used to configure TLS server software.
|
||||||
|
|
||||||
|
Self-signed certificates are generally not trusted by client software such
|
||||||
|
as web browsers. Therefore clients are likely to generate trust warnings when
|
||||||
|
connecting to a server that has a self-signed certificate. Self-signed certificates
|
||||||
|
are usually used only in development environments or apps deployed internally
|
||||||
|
to an organization.
|
||||||
|
|
||||||
|
This resource is intended to be used in conjunction with a Terraform provider
|
||||||
|
that has a resource that requires a TLS certificate, such as:
|
||||||
|
|
||||||
|
* ``aws_iam_server_certificate`` to register certificates for use with AWS *Elastic
|
||||||
|
Load Balancer*, *Elastic Beanstalk*, *CloudFront* or *OpsWorks*.
|
||||||
|
|
||||||
|
* ``heroku_cert`` to register certificates for applications deployed on Heroku.
|
||||||
|
|
||||||
|
## Example Usage
|
||||||
|
|
||||||
|
```
|
||||||
|
resource "tls_self_signed_cert" "example" {
|
||||||
|
key_algorithm = "ECDSA"
|
||||||
|
private_key_pem = "${file(\"private_key.pem\")}"
|
||||||
|
|
||||||
|
subject {
|
||||||
|
common_name = "example.com"
|
||||||
|
organization = "ACME Examples, Inc"
|
||||||
|
}
|
||||||
|
|
||||||
|
validity_period_hours = 12
|
||||||
|
|
||||||
|
allowed_uses = [
|
||||||
|
"key_encipherment",
|
||||||
|
"digital_signature",
|
||||||
|
"server_auth",
|
||||||
|
]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Argument Reference
|
||||||
|
|
||||||
|
The following arguments are supported:
|
||||||
|
|
||||||
|
* `key_algorithm` - (Required) The name of the algorithm for the key provided
|
||||||
|
in `private_key_pem`.
|
||||||
|
|
||||||
|
* `private_key_pem` - (Required) PEM-encoded private key data. This can be
|
||||||
|
read from a separate file using the ``file`` interpolation function. If the
|
||||||
|
certificate is being generated to be used for a throwaway development
|
||||||
|
environment or other non-critical application, the `tls_private_key` resource
|
||||||
|
can be used to generate a TLS private key from within Terraform. Only
|
||||||
|
an irreversable secure hash of the private key will be stored in the Terraform
|
||||||
|
state.
|
||||||
|
|
||||||
|
* `subject` - (Required) The subject for which a certificate is being requested. This is
|
||||||
|
a nested configuration block whose structure is described below.
|
||||||
|
|
||||||
|
* `validity_period_hours` - (Required) The number of hours after initial issuing that the
|
||||||
|
certificate will become invalid.
|
||||||
|
|
||||||
|
* `allowed_uses` - (Required) List of keywords each describing a use that is permitted
|
||||||
|
for the issued certificate. The valid keywords are listed below.
|
||||||
|
|
||||||
|
* `dns_names` - (Optional) List of DNS names for which a certificate is being requested.
|
||||||
|
|
||||||
|
* `ip_addresses` - (Optional) List of IP addresses for which a certificate is being requested.
|
||||||
|
|
||||||
|
* `early_renewal_hours` - (Optional) If set, the resource will consider the certificate to
|
||||||
|
have expired the given number of hours before its actual expiry time. This can be useful
|
||||||
|
to deploy an updated certificate in advance of the expiration of the current certificate.
|
||||||
|
Note however that the old certificate remains valid until its true expiration time, since
|
||||||
|
this resource does not (and cannot) support certificate revocation. Note also that this
|
||||||
|
advance update can only be performed should the Terraform configuration be applied during the
|
||||||
|
early renewal period.
|
||||||
|
|
||||||
|
* `is_ca_certificate` - (Optional) Boolean controlling whether the CA flag will be set in the
|
||||||
|
generated certificate. Defaults to `false`, meaning that the certificate does not represent
|
||||||
|
a certificate authority.
|
||||||
|
|
||||||
|
The `allowed_uses` list accepts the following keywords, combining the set of flags defined by
|
||||||
|
both [Key Usage](https://tools.ietf.org/html/rfc5280#section-4.2.1.3) and
|
||||||
|
[Extended Key Usage](https://tools.ietf.org/html/rfc5280#section-4.2.1.12) in
|
||||||
|
[RFC5280](https://tools.ietf.org/html/rfc5280):
|
||||||
|
|
||||||
|
* `digital_signature`
|
||||||
|
* `content_commitment`
|
||||||
|
* `key_encipherment`
|
||||||
|
* `data_encipherment`
|
||||||
|
* `key_agreement`
|
||||||
|
* `cert_signing`
|
||||||
|
* `encipher_only`
|
||||||
|
* `decipher_only`
|
||||||
|
* `any_extended`
|
||||||
|
* `server_auth`
|
||||||
|
* `client_auth`
|
||||||
|
* `code_signing`
|
||||||
|
* `email_protection`
|
||||||
|
* `ipsec_end_system`
|
||||||
|
* `ipsec_tunnel`
|
||||||
|
* `ipsec_user`
|
||||||
|
* `timestamping`
|
||||||
|
* `ocsp_signing`
|
||||||
|
* `microsoft_server_gated_crypto`
|
||||||
|
* `netscape_server_gated_crypto`
|
||||||
|
|
||||||
|
## Attributes Reference
|
||||||
|
|
||||||
|
The following attributes are exported:
|
||||||
|
|
||||||
|
* `cert_pem` - The certificate data in PEM format.
|
||||||
|
* `validity_start_time` - The time after which the certificate is valid, as an
|
||||||
|
[RFC3339](https://tools.ietf.org/html/rfc3339) timestamp.
|
||||||
|
* `validity_end_time` - The time until which the certificate is invalid, as an
|
||||||
|
[RFC3339](https://tools.ietf.org/html/rfc3339) timestamp.
|
||||||
|
|
||||||
|
## Automatic Renewal
|
||||||
|
|
||||||
|
This resource considers its instances to have been deleted after either their validity
|
||||||
|
periods ends or the early renewal period is reached. At this time, applying the
|
||||||
|
Terraform configuration will cause a new certificate to be generated for the instance.
|
||||||
|
|
||||||
|
Therefore in a development environment with frequent deployments it may be convenient
|
||||||
|
to set a relatively-short expiration time and use early renewal to automatically provision
|
||||||
|
a new certificate when the current one is about to expire.
|
||||||
|
|
||||||
|
The creation of a new certificate may of course cause dependent resources to be updated
|
||||||
|
or replaced, depending on the lifecycle rules applying to those resources.
|
|
@ -189,6 +189,10 @@
|
||||||
<a href="/docs/providers/template/index.html">Template</a>
|
<a href="/docs/providers/template/index.html">Template</a>
|
||||||
</li>
|
</li>
|
||||||
|
|
||||||
|
<li<%= sidebar_current("docs-providers-tls") %>>
|
||||||
|
<a href="/docs/providers/tls/index.html">TLS</a>
|
||||||
|
</li>
|
||||||
|
|
||||||
<li<%= sidebar_current("docs-providers-vsphere") %>>
|
<li<%= sidebar_current("docs-providers-vsphere") %>>
|
||||||
<a href="/docs/providers/vsphere/index.html">vSphere</a>
|
<a href="/docs/providers/vsphere/index.html">vSphere</a>
|
||||||
</li>
|
</li>
|
||||||
|
|
|
@ -0,0 +1,32 @@
|
||||||
|
<% wrap_layout :inner do %>
|
||||||
|
<% content_for :sidebar do %>
|
||||||
|
<div class="docs-sidebar hidden-print affix-top" role="complementary">
|
||||||
|
<ul class="nav docs-sidenav">
|
||||||
|
<li<%#= sidebar_current("docs-home") %>>
|
||||||
|
<a href="/docs/providers/index.html">« Documentation Home</a>
|
||||||
|
</li>
|
||||||
|
|
||||||
|
<li<%= sidebar_current("docs-tls-index") %>>
|
||||||
|
<a href="/docs/providers/tls/index.html">TLS Provider</a>
|
||||||
|
</li>
|
||||||
|
|
||||||
|
<li<%= sidebar_current(/^docs-tls-resource/) %>>
|
||||||
|
<a href="#">Resources</a>
|
||||||
|
<ul class="nav nav-visible">
|
||||||
|
<li<%= sidebar_current("docs-tls-resourse-private-key") %>>
|
||||||
|
<a href="/docs/providers/tls/r/private_key.html">tls_private_key</a>
|
||||||
|
</li>
|
||||||
|
<li<%= sidebar_current("docs-tls-resourse-self-signed-cert") %>>
|
||||||
|
<a href="/docs/providers/tls/r/self_signed_cert.html">tls_self_signed_cert</a>
|
||||||
|
</li>
|
||||||
|
<li<%= sidebar_current("docs-tls-resourse-cert-request") %>>
|
||||||
|
<a href="/docs/providers/tls/r/cert_request.html">tls_cert_request</a>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
<% end %>
|
||||||
|
|
||||||
|
<%= yield %>
|
||||||
|
<% end %>
|
Loading…
Reference in New Issue