Skip to content

Commit

Permalink
provider/acme: New provider
Browse files Browse the repository at this point in the history
This commit adds the ACME provider, enabling support for automated
domain-validated certificate authorities such as Let's Encrypt
(https://letsencrypt.org/).

There are 2 resources:

 * acme_registration - This resource manages registrations with an ACME
   CA, which in the resource is a private key/email contact combination.
 * acme_certificate - This resource manages certificates, taking domains
   directly via the resource or through a pre-generated CSR. HTTP, TLS,
   and DNS challenges are supported, the latter through an assortment of
   providers.

There is no explicit provider configuration for this provider, to
address the chicken-and-egg relationship between registrations and
certificates. Neither resource has a hard dependency on the other and
acme_certificate can use a registration that is not managed by
Terraform.

Consult the ACME provider documentation at
website/source/docs/providers/acme for full details, or at
https://www.terraform.io/docs/providers/acme/index.html once the merged
documentation is online.

Also, as part of this commit, we flag several private key-related fields
in the resources in the TLS provider as sensitive, so that they can be
used within the resource should the need be there, ensuring that their
field data does not get leaked in logs.
  • Loading branch information
Chris Marchesi committed Jun 15, 2016
1 parent 4796e27 commit 02fdb83
Show file tree
Hide file tree
Showing 153 changed files with 45,139 additions and 2 deletions.
612 changes: 612 additions & 0 deletions builtin/providers/acme/acme_structure.go

Large diffs are not rendered by default.

445 changes: 445 additions & 0 deletions builtin/providers/acme/acme_structure_test.go

Large diffs are not rendered by default.

17 changes: 17 additions & 0 deletions builtin/providers/acme/provider.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
package acme

import (
"github.com/hashicorp/terraform/helper/schema"
"github.com/hashicorp/terraform/terraform"
)

// Provider returns the terraform.ResourceProvider structure for the ACME
// provider.
func Provider() terraform.ResourceProvider {
return &schema.Provider{
ResourcesMap: map[string]*schema.Resource{
"acme_registration": resourceACMERegistration(),
"acme_certificate": resourceACMECertificate(),
},
}
}
32 changes: 32 additions & 0 deletions builtin/providers/acme/provider_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
package acme

import (
"testing"

"github.com/hashicorp/terraform/builtin/providers/aws"
"github.com/hashicorp/terraform/builtin/providers/tls"
"github.com/hashicorp/terraform/helper/schema"
"github.com/hashicorp/terraform/terraform"
)

var testAccProvider *schema.Provider
var testAccProviders map[string]terraform.ResourceProvider

func init() {
testAccProvider = Provider().(*schema.Provider)
testAccProviders = map[string]terraform.ResourceProvider{
"acme": testAccProvider,
"tls": tls.Provider().(*schema.Provider),
"aws": aws.Provider().(*schema.Provider),
}
}

func TestProvider(t *testing.T) {
if err := Provider().(*schema.Provider).InternalValidate(); err != nil {
t.Fatalf("err: %s", err)
}
}

func TestProvider_impl(t *testing.T) {
var _ terraform.ResourceProvider = Provider()
}
169 changes: 169 additions & 0 deletions builtin/providers/acme/resource_acme_certificate.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,169 @@
package acme

import (
"fmt"
"log"
"strconv"
"strings"
"time"

"golang.org/x/crypto/ocsp"

"github.com/hashicorp/terraform/helper/resource"
"github.com/hashicorp/terraform/helper/schema"
"github.com/xenolf/lego/acme"
)

func resourceACMECertificate() *schema.Resource {
return &schema.Resource{
Create: resourceACMECertificateCreate,
Read: resourceACMECertificateRead,
Delete: resourceACMECertificateDelete,

Schema: certificateSchemaFull(),
}
}

func resourceACMECertificateCreate(d *schema.ResourceData, meta interface{}) error {
client, _, err := expandACMEClient(d, d.Get("registration_url").(string))
if err != nil {
return err
}

if v, ok := d.GetOk("dns_challenge"); ok {
setDNSChallenge(client, v.(*schema.Set).List()[0].(map[string]interface{}))
} else {
client.SetHTTPAddress(":" + strconv.Itoa(d.Get("http_challenge_port").(int)))
client.SetTLSAddress(":" + strconv.Itoa(d.Get("tls_challenge_port").(int)))
}

var cert acme.CertificateResource
var errs map[string]error

if v, ok := d.GetOk("certificate_request_pem"); ok {
csr, err := csrFromPEM([]byte(v.(string)))
if err != nil {
return err
}
cert, errs = client.ObtainCertificateForCSR(*csr, true)
} else {
cn := d.Get("common_name").(string)
domains := []string{cn}
if s, ok := d.GetOk("subject_alternative_names"); ok {
for _, v := range stringSlice(s.(*schema.Set).List()) {
if v == cn {
return fmt.Errorf("common name %s should not appear in SAN list", v)
}
domains = append(domains, v)
}
}

cert, errs = client.ObtainCertificate(domains, true, nil)
}

if len(errs) > 0 {
messages := []string{}
for k, v := range errs {
messages = append(messages, fmt.Sprintf("%s: %s", k, v))
}
return fmt.Errorf("Errors were encountered creating the certificate:\n %s", strings.Join(messages, "\n "))
}

// done! save the cert
saveCertificateResource(d, cert)

return nil
}

// resourceACMECertificateRead renews the certificate if it is close to expiry.
// This value is controlled by the min_days_remaining attribute - if this value
// less than zero, the certificate is never renewed.
func resourceACMECertificateRead(d *schema.ResourceData, meta interface{}) error {
mindays := d.Get("min_days_remaining").(int)
if mindays < 0 {
log.Printf("[WARN] min_days_remaining is set to less than 0, certificate will never be renewed")
return nil
}

client, _, err := expandACMEClient(d, d.Get("registration_url").(string))
if err != nil {
return err
}

cert := expandCertificateResource(d)
remaining, err := certDaysRemaining(cert)
if err != nil {
return err
}

if int64(mindays) >= remaining {
if v, ok := d.GetOk("dns_challenge"); ok {
setDNSChallenge(client, v.(*schema.Set).List()[0].(map[string]interface{}))
} else {
client.SetHTTPAddress(":" + strconv.Itoa(d.Get("http_challenge_port").(int)))
client.SetTLSAddress(":" + strconv.Itoa(d.Get("tls_challenge_port").(int)))
}
newCert, err := client.RenewCertificate(cert, true)
if err != nil {
return err
}
saveCertificateResource(d, newCert)
}

return nil
}

// resourceACMECertificateDelete "deletes" the certificate by revoking it.
func resourceACMECertificateDelete(d *schema.ResourceData, meta interface{}) error {
client, _, err := expandACMEClient(d, d.Get("registration_url").(string))
if err != nil {
return err
}

cert, ok := d.GetOk("certificate_pem")

if ok {
err = client.RevokeCertificate([]byte(cert.(string)))
if err != nil {
return err
}
}

// Add a state waiter for the OCSP status of the cert, to make sure it's
// truly revoked.
state := &resource.StateChangeConf{
Pending: []string{"Good"},
Target: []string{"Revoked"},
Refresh: resourceACMECertificateRevokeRefreshFunc(cert.(string)),
Timeout: 3 * time.Minute,
MinTimeout: 15 * time.Second,
Delay: 5 * time.Second,
}

_, err = state.WaitForState()
if err != nil {
return fmt.Errorf("Cert did not revoke: %s", err.Error())
}

d.SetId("")
return nil
}

// resourceACMECertificateRevokeRefreshFunc polls the certificate's status
// via the OSCP url and returns success once it's Revoked.
func resourceACMECertificateRevokeRefreshFunc(cert string) resource.StateRefreshFunc {
return func() (interface{}, string, error) {
_, resp, err := acme.GetOCSPForCert([]byte(cert))
if err != nil {
return nil, "", fmt.Errorf("Bad: %s", err.Error())
}
switch resp.Status {
case ocsp.Revoked:
return cert, "Revoked", nil
case ocsp.Good:
return cert, "Good", nil
default:
return nil, "", fmt.Errorf("Bad status: OCSP status %d", resp.Status)
}
}
}
Loading

0 comments on commit 02fdb83

Please sign in to comment.