From f6fd41e7b54b38176090557c4a13e18c86e46fbe Mon Sep 17 00:00:00 2001 From: Martin Atkins Date: Sat, 18 Jul 2015 11:50:13 -0700 Subject: [PATCH] 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. --- builtin/bins/provider-tls/main.go | 12 + builtin/providers/tls/provider.go | 111 ++++++++ builtin/providers/tls/provider_test.go | 36 +++ .../providers/tls/resource_cert_request.go | 135 +++++++++ .../tls/resource_cert_request_test.go | 115 ++++++++ builtin/providers/tls/resource_private_key.go | 144 ++++++++++ .../tls/resource_private_key_test.go | 84 ++++++ .../tls/resource_self_signed_cert.go | 265 ++++++++++++++++++ .../tls/resource_self_signed_cert_test.go | 152 ++++++++++ website/source/assets/stylesheets/_docs.scss | 1 + .../docs/providers/tls/index.html.markdown | 72 +++++ .../docs/providers/tls/r/cert_request.html.md | 78 ++++++ .../docs/providers/tls/r/private_key.html.md | 66 +++++ .../providers/tls/r/self_signed_cert.html.md | 137 +++++++++ website/source/layouts/docs.erb | 8 +- website/source/layouts/tls.erb | 32 +++ 16 files changed, 1446 insertions(+), 2 deletions(-) create mode 100644 builtin/bins/provider-tls/main.go create mode 100644 builtin/providers/tls/provider.go create mode 100644 builtin/providers/tls/provider_test.go create mode 100644 builtin/providers/tls/resource_cert_request.go create mode 100644 builtin/providers/tls/resource_cert_request_test.go create mode 100644 builtin/providers/tls/resource_private_key.go create mode 100644 builtin/providers/tls/resource_private_key_test.go create mode 100644 builtin/providers/tls/resource_self_signed_cert.go create mode 100644 builtin/providers/tls/resource_self_signed_cert_test.go create mode 100644 website/source/docs/providers/tls/index.html.markdown create mode 100644 website/source/docs/providers/tls/r/cert_request.html.md create mode 100644 website/source/docs/providers/tls/r/private_key.html.md create mode 100644 website/source/docs/providers/tls/r/self_signed_cert.html.md create mode 100644 website/source/layouts/tls.erb diff --git a/builtin/bins/provider-tls/main.go b/builtin/bins/provider-tls/main.go new file mode 100644 index 000000000..7fb32d31e --- /dev/null +++ b/builtin/bins/provider-tls/main.go @@ -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, + }) +} diff --git a/builtin/providers/tls/provider.go b/builtin/providers/tls/provider.go new file mode 100644 index 000000000..69dfa0ded --- /dev/null +++ b/builtin/providers/tls/provider.go @@ -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, + }, + }, +} diff --git a/builtin/providers/tls/provider_test.go b/builtin/providers/tls/provider_test.go new file mode 100644 index 000000000..31b014733 --- /dev/null +++ b/builtin/providers/tls/provider_test.go @@ -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----- +` diff --git a/builtin/providers/tls/resource_cert_request.go b/builtin/providers/tls/resource_cert_request.go new file mode 100644 index 000000000..ac1f70071 --- /dev/null +++ b/builtin/providers/tls/resource_cert_request.go @@ -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 +} diff --git a/builtin/providers/tls/resource_cert_request_test.go b/builtin/providers/tls/resource_cert_request_test.go new file mode 100644 index 000000000..5ddad805c --- /dev/null +++ b/builtin/providers/tls/resource_cert_request_test.go @@ -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 = < 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 + }, + }, + }, + }) +} diff --git a/builtin/providers/tls/resource_self_signed_cert.go b/builtin/providers/tls/resource_self_signed_cert.go new file mode 100644 index 000000000..405535245 --- /dev/null +++ b/builtin/providers/tls/resource_self_signed_cert.go @@ -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 +} diff --git a/builtin/providers/tls/resource_self_signed_cert_test.go b/builtin/providers/tls/resource_self_signed_cert_test.go new file mode 100644 index 000000000..2ba3b2939 --- /dev/null +++ b/builtin/providers/tls/resource_self_signed_cert_test.go @@ -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 = < (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 + }, + }, + }, + }) +} diff --git a/website/source/assets/stylesheets/_docs.scss b/website/source/assets/stylesheets/_docs.scss index 0defd251a..11dff7720 100755 --- a/website/source/assets/stylesheets/_docs.scss +++ b/website/source/assets/stylesheets/_docs.scss @@ -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, diff --git a/website/source/docs/providers/tls/index.html.markdown b/website/source/docs/providers/tls/index.html.markdown new file mode 100644 index 000000000..42017111a --- /dev/null +++ b/website/source/docs/providers/tls/index.html.markdown @@ -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}" +} +``` diff --git a/website/source/docs/providers/tls/r/cert_request.html.md b/website/source/docs/providers/tls/r/cert_request.html.md new file mode 100644 index 000000000..aeb7e1180 --- /dev/null +++ b/website/source/docs/providers/tls/r/cert_request.html.md @@ -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. diff --git a/website/source/docs/providers/tls/r/private_key.html.md b/website/source/docs/providers/tls/r/private_key.html.md new file mode 100644 index 000000000..1a4a2cec4 --- /dev/null +++ b/website/source/docs/providers/tls/r/private_key.html.md @@ -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``. diff --git a/website/source/docs/providers/tls/r/self_signed_cert.html.md b/website/source/docs/providers/tls/r/self_signed_cert.html.md new file mode 100644 index 000000000..8f552e472 --- /dev/null +++ b/website/source/docs/providers/tls/r/self_signed_cert.html.md @@ -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. diff --git a/website/source/layouts/docs.erb b/website/source/layouts/docs.erb index 937c120de..587082776 100644 --- a/website/source/layouts/docs.erb +++ b/website/source/layouts/docs.erb @@ -189,9 +189,13 @@ Template - > - vSphere + > + TLS + + > + vSphere + diff --git a/website/source/layouts/tls.erb b/website/source/layouts/tls.erb new file mode 100644 index 000000000..3892ebbb4 --- /dev/null +++ b/website/source/layouts/tls.erb @@ -0,0 +1,32 @@ +<% wrap_layout :inner do %> + <% content_for :sidebar do %> + + <% end %> + + <%= yield %> +<% end %>