tls provider

As of this commit this provider has only logical resources that allow
the creation of private keys, self-signed certs and certificate requests.
These can be useful when creating other resources that use TLS
certificates, such as AWS Elastic Load Balancers.

Later it could grow to include support for real certificate provision from
CAs using the LetsEncrypt ACME protocol, once it is stable.
This commit is contained in:
Martin Atkins 2015-07-18 11:50:13 -07:00
parent 864b2dee01
commit f6fd41e7b5
16 changed files with 1446 additions and 2 deletions

View File

@ -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,
})
}

View File

@ -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,
},
},
}

View File

@ -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-----
`

View File

@ -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
}

View File

@ -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
},
},
},
})
}

View File

@ -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
}
}

View File

@ -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
},
},
},
})
}

View File

@ -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
}

View File

@ -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
},
},
},
})
}

View File

@ -23,6 +23,7 @@ body.layout-openstack,
body.layout-packet,
body.layout-rundeck,
body.layout-template,
body.layout-tls,
body.layout-vsphere,
body.layout-docs,
body.layout-downloads,

View File

@ -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}"
}
```

View File

@ -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.

View File

@ -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``.

View File

@ -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.

View File

@ -189,9 +189,13 @@
<a href="/docs/providers/template/index.html">Template</a>
</li>
<li<%= sidebar_current("docs-providers-vsphere") %>>
<a href="/docs/providers/vsphere/index.html">vSphere</a>
<li<%= sidebar_current("docs-providers-tls") %>>
<a href="/docs/providers/tls/index.html">TLS</a>
</li>
<li<%= sidebar_current("docs-providers-vsphere") %>>
<a href="/docs/providers/vsphere/index.html">vSphere</a>
</li>
</ul>
</li>

View File

@ -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">&laquo; 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 %>