Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -29,3 +29,4 @@ website/vendor
# Test exclusions
!command/test-fixtures/**/*.tfstate
!command/test-fixtures/**/.terraform/
/terraform-provider-tls
6 changes: 5 additions & 1 deletion docs/resources/self_signed_cert.md
Original file line number Diff line number Diff line change
Expand Up @@ -42,15 +42,19 @@ resource "tls_self_signed_cert" "example" {
### Required

- `allowed_uses` (List of String) List of key usages allowed for the issued certificate. Values are defined in [RFC 5280](https://datatracker.ietf.org/doc/html/rfc5280) and combine flags defined by both [Key Usages](https://datatracker.ietf.org/doc/html/rfc5280#section-4.2.1.3) and [Extended Key Usages](https://datatracker.ietf.org/doc/html/rfc5280#section-4.2.1.12). Accepted values: `any_extended`, `cert_signing`, `client_auth`, `code_signing`, `content_commitment`, `crl_signing`, `data_encipherment`, `decipher_only`, `digital_signature`, `email_protection`, `encipher_only`, `ipsec_end_system`, `ipsec_tunnel`, `ipsec_user`, `key_agreement`, `key_encipherment`, `microsoft_commercial_code_signing`, `microsoft_kernel_code_signing`, `microsoft_server_gated_crypto`, `netscape_server_gated_crypto`, `ocsp_signing`, `server_auth`, `timestamping`.
- `private_key_pem` (String, Sensitive) Private key in [PEM (RFC 1421)](https://datatracker.ietf.org/doc/html/rfc1421) format, that the certificate will belong to. This can be read from a separate file using the [`file`](https://www.terraform.io/language/functions/file) interpolation function.
- `validity_period_hours` (Number) Number of hours, after initial issuing, that the certificate will remain valid for.

### Optional

> **NOTE**: [Write-only arguments](https://developer.hashicorp.com/terraform/language/resources/ephemeral#write-only-arguments) are supported in Terraform 1.11 and later.

- `dns_names` (List of String) List of DNS names for which a certificate is being requested (i.e. certificate subjects).
- `early_renewal_hours` (Number) 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. However, the old certificate remains valid until its true expiration time, since this resource does not (and cannot) support certificate revocation. Also, this advance update can only be performed should the Terraform configuration be applied during the early renewal period. (default: `0`)
- `ip_addresses` (List of String) List of IP addresses for which a certificate is being requested (i.e. certificate subjects).
- `is_ca_certificate` (Boolean) Is the generated certificate representing a Certificate Authority (CA) (default: `false`).
- `private_key_pem` (String, Sensitive) Private key in [PEM (RFC 1421)](https://datatracker.ietf.org/doc/html/rfc1421) format, that the certificate will belong to. This can be read from a separate file using the [`file`](https://www.terraform.io/language/functions/file) interpolation function.
- `private_key_pem_wo` (String, Sensitive, [Write-only](https://developer.hashicorp.com/terraform/language/resources/ephemeral#write-only-arguments)) Write only variant of private_key_pem.
- `private_key_pem_wo_version` (Number) Triggers update of private_key_pem write-only.
- `set_authority_key_id` (Boolean) Should the generated certificate include an [authority key identifier](https://datatracker.ietf.org/doc/html/rfc5280#section-4.2.1.1): for self-signed certificates this is the same value as the [subject key identifier](https://datatracker.ietf.org/doc/html/rfc5280#section-4.2.1.2) (default: `false`).
- `set_subject_key_id` (Boolean) Should the generated certificate include a [subject key identifier](https://datatracker.ietf.org/doc/html/rfc5280#section-4.2.1.2) (default: `false`).
- `subject` (Block List) The subject for which a certificate is being requested. The acceptable arguments are all optional and their naming is based upon [Issuer Distinguished Names (RFC5280)](https://tools.ietf.org/html/rfc5280#section-4.1.2.4) section. (see [below for nested schema](#nestedblock--subject))
Expand Down
36 changes: 19 additions & 17 deletions internal/provider/models.go
Original file line number Diff line number Diff line change
Expand Up @@ -184,23 +184,25 @@ func (data *privateKeyEphemeralModel) toResourceModel() privateKeyResourceModel
}

type selfSignedCertResourceModel struct {
PrivateKeyPEM types.String `tfsdk:"private_key_pem"`
DNSNames types.List `tfsdk:"dns_names"`
IPAddresses types.List `tfsdk:"ip_addresses"`
URIs types.List `tfsdk:"uris"`
Subject types.List `tfsdk:"subject"` //< certificateSubjectModel
ValidityPeriodHours types.Int64 `tfsdk:"validity_period_hours"`
AllowedUses types.List `tfsdk:"allowed_uses"`
EarlyRenewalHours types.Int64 `tfsdk:"early_renewal_hours"`
IsCACertificate types.Bool `tfsdk:"is_ca_certificate"`
SetSubjectKeyID types.Bool `tfsdk:"set_subject_key_id"`
SetAuthorityKeyID types.Bool `tfsdk:"set_authority_key_id"`
CertPEM types.String `tfsdk:"cert_pem"`
ReadyForRenewal types.Bool `tfsdk:"ready_for_renewal"`
ValidityStartTime types.String `tfsdk:"validity_start_time"`
ValidityEndTime types.String `tfsdk:"validity_end_time"`
KeyAlgorithm types.String `tfsdk:"key_algorithm"`
ID types.String `tfsdk:"id"`
PrivateKeyPEM types.String `tfsdk:"private_key_pem"`
PrivateKeyPEMWO types.String `tfsdk:"private_key_pem_wo"`
PrivateKeyPEMWOVersion types.Int64 `tfsdk:"private_key_pem_wo_version"`
DNSNames types.List `tfsdk:"dns_names"`
IPAddresses types.List `tfsdk:"ip_addresses"`
URIs types.List `tfsdk:"uris"`
Subject types.List `tfsdk:"subject"` //< certificateSubjectModel
ValidityPeriodHours types.Int64 `tfsdk:"validity_period_hours"`
AllowedUses types.List `tfsdk:"allowed_uses"`
EarlyRenewalHours types.Int64 `tfsdk:"early_renewal_hours"`
IsCACertificate types.Bool `tfsdk:"is_ca_certificate"`
SetSubjectKeyID types.Bool `tfsdk:"set_subject_key_id"`
SetAuthorityKeyID types.Bool `tfsdk:"set_authority_key_id"`
CertPEM types.String `tfsdk:"cert_pem"`
ReadyForRenewal types.Bool `tfsdk:"ready_for_renewal"`
ValidityStartTime types.String `tfsdk:"validity_start_time"`
ValidityEndTime types.String `tfsdk:"validity_end_time"`
KeyAlgorithm types.String `tfsdk:"key_algorithm"`
ID types.String `tfsdk:"id"`
}

type locallySignedCertResourceModel struct {
Expand Down
55 changes: 52 additions & 3 deletions internal/provider/resource_self_signed_cert.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,11 +9,13 @@ import (
"fmt"
"net"
"net/url"
"regexp"
"strings"

"github.com/hashicorp/terraform-plugin-framework-validators/int64validator"
"github.com/hashicorp/terraform-plugin-framework-validators/listvalidator"
"github.com/hashicorp/terraform-plugin-framework-validators/stringvalidator"
"github.com/hashicorp/terraform-plugin-framework/path"
"github.com/hashicorp/terraform-plugin-framework/resource"
"github.com/hashicorp/terraform-plugin-framework/resource/schema"
"github.com/hashicorp/terraform-plugin-framework/resource/schema/booldefault"
Expand Down Expand Up @@ -46,11 +48,11 @@ func (r *selfSignedCertResource) Metadata(_ context.Context, req resource.Metada
}

func (r *selfSignedCertResource) Schema(_ context.Context, req resource.SchemaRequest, resp *resource.SchemaResponse) {

resp.Schema = schema.Schema{
Attributes: map[string]schema.Attribute{
// Required attributes
"private_key_pem": schema.StringAttribute{
Required: true,
Optional: true,
PlanModifiers: []planmodifier.String{
requireReplaceIfStateContainsPEMString(),
},
Expand All @@ -59,7 +61,47 @@ func (r *selfSignedCertResource) Schema(_ context.Context, req resource.SchemaRe
"that the certificate will belong to. " +
"This can be read from a separate file using the [`file`](https://www.terraform.io/language/functions/file) " +
"interpolation function. ",
Validators: []validator.String{
stringvalidator.ExactlyOneOf(
path.MatchRoot("private_key_pem"),
path.MatchRoot("private_key_pem_wo"),
),
},
},
"private_key_pem_wo": schema.StringAttribute{
Optional: true,
WriteOnly: true,
Sensitive: true,
Description: "Write only variant of private_key_pem.",
Validators: []validator.String{
stringvalidator.ExactlyOneOf(
path.MatchRoot("private_key_pem"),
path.MatchRoot("private_key_pem_wo"),
),
stringvalidator.AlsoRequires(
path.MatchRoot("private_key_pem_wo_version"),
),
stringvalidator.RegexMatches(
regexp.MustCompile(`^-----BEGIN [[:alpha:] ]+-----\n(.|\s)+\n-----END [[:alpha:] ]+-----\n?$`),
"must be a valid PEM string",
),
},
},
"private_key_pem_wo_version": schema.Int64Attribute{
Required: false,
Optional: true,
PlanModifiers: []planmodifier.Int64{
int64planmodifier.RequiresReplace(),
},
Validators: []validator.Int64{
int64validator.AlsoRequires(
path.MatchRoot("private_key_pem_wo"),
),
},
Sensitive: false,
Description: "Triggers update of the certificate using the private_key_pem_wo write-only value.",
},

"validity_period_hours": schema.Int64Attribute{
Required: true,
PlanModifiers: []planmodifier.Int64{
Expand Down Expand Up @@ -331,9 +373,16 @@ func (r *selfSignedCertResource) Create(ctx context.Context, req resource.Create
"selfSignedCertConfig": fmt.Sprintf("%+v", newState),
})

privateKeyPEM := ""
diagnostics := req.Config.GetAttribute(ctx, path.Root("private_key_pem_wo"), &privateKeyPEM)
if privateKeyPEM == "" || diagnostics.HasError() {
tflog.Debug(ctx, "private_key_pem_wo not set, using private_key_pem instead")
privateKeyPEM = newState.PrivateKeyPEM.ValueString()
}

// Parse the Private Key PEM
tflog.Debug(ctx, "Parsing private key PEM")
prvKey, algorithm, err := parsePrivateKeyPEM([]byte(newState.PrivateKeyPEM.ValueString()))
prvKey, algorithm, err := parsePrivateKeyPEM([]byte(privateKeyPEM))
if err != nil {
res.Diagnostics.AddError("Failed to parse private key PEM", err.Error())
return
Expand Down
33 changes: 33 additions & 0 deletions internal/provider/resource_self_signed_cert_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -914,3 +914,36 @@ func TestResourceSelfSignedCert_NoSubject(t *testing.T) {
},
})
}

func TestResourceSelfSignedCert_UsingWriteOnlyEphemeralPrivateKey(t *testing.T) {
r.UnitTest(t, r.TestCase{
ProtoV5ProviderFactories: protoV5ProviderFactories(),
Steps: []r.TestStep{
{
Config: `
ephemeral "tls_private_key" "test" {
algorithm = "RSA"
rsa_bits = 4096
}
resource "tls_self_signed_cert" "test" {
private_key_pem_wo = ephemeral.tls_private_key.test.private_key_pem
private_key_pem_wo_version = 1
subject {
organization = "test-organization"
}
is_ca_certificate = true
set_subject_key_id = true
validity_period_hours = 8760
allowed_uses = [
"cert_signing",
]
}
`,
Check: r.ComposeAggregateTestCheckFunc(
r.TestCheckResourceAttr("tls_self_signed_cert.test", "key_algorithm", "RSA"),
tu.TestCheckPEMFormat("tls_self_signed_cert.test", "cert_pem", PreambleCertificate.String()),
),
},
},
})
}