Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Cannot rotate certificate with routeros_system_certificate import #584

Open
satwell opened this issue Oct 11, 2024 · 4 comments
Open

Cannot rotate certificate with routeros_system_certificate import #584

satwell opened this issue Oct 11, 2024 · 4 comments
Labels
bug Something isn't working

Comments

@satwell
Copy link

satwell commented Oct 11, 2024

Describe the bug
I'm trying to manage the TLS certificate on a RouterOS 7.16 device with terraform-routeros/routeros 1.65.1. I create the key and certificate externally and then import with Terraform using routeros_system_certificate with import. The initial import works fine. But when I try to rotate the certificate, the certificate on RouterOS isn't updated, even though the routeros_file resources do get updated.

To Reproduce
This example uses the hashicorp/tls provider to implement a basic CA. Then it creates a key and CSR, signs the CSR with the CA, and imports the resulting signed certificate and key into RouterOS. I output the serial of the certificate on RouterOS and the serial of the certificate created by the tls provider to make it easy to compare.

resource "tls_private_key" "ca_key" {
  algorithm = "RSA"
}

resource "tls_self_signed_cert" "ca_cert" {
  subject {
    common_name  = "testCA"
    organization = "test"
  }

  private_key_pem       = tls_private_key.ca_key.private_key_pem
  allowed_uses          = ["digital_signature", "cert_signing", "crl_signing"]
  validity_period_hours = 24 * 365 * 5
  is_ca_certificate     = true
}

resource "tls_private_key" "server_key" {
  algorithm = "RSA"
}

resource "tls_cert_request" "server_csr" {
  private_key_pem = tls_private_key.server_key.private_key_pem
  subject {
    common_name  = "mikrotik.example.com"
    organization = "test"
  }
}

resource "tls_locally_signed_cert" "server_cert" {
  cert_request_pem   = tls_cert_request.server_csr.cert_request_pem
  ca_private_key_pem = tls_private_key.ca_key.private_key_pem
  ca_cert_pem        = tls_self_signed_cert.ca_cert.cert_pem

  validity_period_hours = 12

  allowed_uses = [
    "key_encipherment",
    "digital_signature",
    "server_auth",
  ]
}

output "cert_serial_number_expected" {
  value = format("%x", tls_locally_signed_cert.server_cert.id)
}

resource "routeros_file" "server_key" {
  name     = "server.key"
  contents = tls_private_key.server_key.private_key_pem
}

resource "routeros_file" "server_cert" {
  name     = "server.crt"
  contents = tls_locally_signed_cert.server_cert.cert_pem
}

resource "routeros_system_certificate" "server_cert" {
  name        = "server"
  common_name = tls_cert_request.server_csr.subject[0].common_name
  import {
    cert_file_name = routeros_file.server_cert.name
    key_file_name  = routeros_file.server_key.name
  }
  depends_on = [routeros_file.server_cert, routeros_file.server_key]
}

output "cert_serial_nubmer_on_device" {
  value = routeros_system_certificate.server_cert.serial_number
}

The initial apply works as expected:

Apply complete! Resources: 8 added, 0 changed, 0 destroyed.

Outputs:

cert_serial_nubmer_on_device = "ab861f4b855f1927ff0e77692354c921"
cert_serial_number_expected = "ab861f4b855f1927ff0e77692354c921"

And this matches the serial-number on the device:

[admin@MikroTik] > /certificate/print detail where name="server" 
Flags: K - private-key; L - crl; C - smart-card-key; A - authority; I - issued, R - revoked; E - expired; T - trusted 
 0 K     T name="server" issuer=O=test,CN=testCA digest-algorithm=sha256 key-type=rsa organization="test" 
           common-name="mikrotik.example.com" key-size=2048 subject-alt-name="" days-valid=0 trusted=yes 
           key-usage=digital-signature,key-encipherment,tls-server serial-number="ab861f4b855f1927ff0e77692354c921" 
           fingerprint="0474f72ce80668e1c6b27d275cac9bdfc0f596bbfe1cdaa441fa152ff8c8b80d" 
           akid=004019c260225e8c2e955517d69c21c3a4b9aa33 skid="" invalid-before=2024-10-11 16:45:35 
           invalid-after=2024-10-12 04:45:35 expires-after=11h56m50s 

Now I'll rotate the certificate by tainting the tls_locally_signed_cert to trigger creation of a new certificate:

$ terraform taint tls_locally_signed_cert.server_cert
Resource instance tls_locally_signed_cert.server_cert has been marked as tainted.

terraform plan tells me that routeros_file.server_key and routeros_file.server_cert will created, and tls_locally_signed_cert.server_cert will be replaced. For some reason it doesn't replace or update routeros_system_certificate.server_cert:

$ terraform apply
[...]
tls_locally_signed_cert.server_cert: Destroying... [id=227994389796350655996448688647229983009]
tls_locally_signed_cert.server_cert: Destruction complete after 0s
routeros_file.server_key: Creating...
routeros_file.server_key: Creation complete after 0s [id=*FE02001D]
tls_locally_signed_cert.server_cert: Creating...
tls_locally_signed_cert.server_cert: Creation complete after 0s [id=223615023624951024914163690532214841200]
routeros_file.server_cert: Creating...
routeros_file.server_cert: Creation complete after 0s [id=*FE02002D]

Apply complete! Resources: 3 added, 0 changed, 1 destroyed.

Outputs:

cert_serial_nubmer_on_device = "ab861f4b855f1927ff0e77692354c921"
cert_serial_number_expected = "a83aafdf5322be6c4ed0a2dcd2ca9f70"

And notice that serial numbers don't match any more. The device is still using the old certificate, which we can confirm by running /certificate/print detail where name="server" again.

Just to be thorough, we can also verify that the uploaded routeros_file resources contain the new certificate. Let's import it with a different name to keep it separate:

[admin@MikroTik] /certificate> import name=updated file-name=server.crt          
     certificates-imported: 1
     private-keys-imported: 0
            files-imported: 0
       decryption-failures: 0
  keys-with-no-certificate: 0

[admin@MikroTik] /certificate> import name=updated file-name=server.key  
     certificates-imported: 0
     private-keys-imported: 1
            files-imported: 1
       decryption-failures: 0
  keys-with-no-certificate: 0

[admin@MikroTik] /certificate> print detail 
Flags: K - private-key; L - crl; C - smart-card-key; A - authority; I - issued, R - revoked; E - expired; T - trusted 
 0 K     T name="server" issuer=O=test,CN=testCA digest-algorithm=sha256 key-type=rsa organization="test" 
           common-name="mikrotik.example.com" key-size=2048 subject-alt-name="" days-valid=0 trusted=yes 
           key-usage=digital-signature,key-encipherment,tls-server serial-number="ab861f4b855f1927ff0e77692354c921" 
           fingerprint="0474f72ce80668e1c6b27d275cac9bdfc0f596bbfe1cdaa441fa152ff8c8b80d" 
           akid=004019c260225e8c2e955517d69c21c3a4b9aa33 skid="" invalid-before=2024-10-11 16:45:35 
           invalid-after=2024-10-12 04:45:35 expires-after=11h40m41s 

 1 K     T name="updated" issuer=O=test,CN=testCA digest-algorithm=sha256 key-type=rsa organization="test" 
           common-name="mikrotik.example.com" key-size=2048 subject-alt-name="" days-valid=0 trusted=yes 
           key-usage=digital-signature,key-encipherment,tls-server serial-number="a83aafdf5322be6c4ed0a2dcd2ca9f70" 
           fingerprint="153c6b9740a8eadc773d81ba802acdca5ca46251e9368ddb95e92baa83369c47" 
           akid=004019c260225e8c2e955517d69c21c3a4b9aa33 skid="" invalid-before=2024-10-11 16:58:07 
           invalid-after=2024-10-12 04:58:07 expires-after=11h53m13s 

Expected behavior
Updating the imported certificates in Terraform should result in the certificates being updated in RouterOS.

@satwell satwell added the bug Something isn't working label Oct 11, 2024
@vaerh
Copy link
Collaborator

vaerh commented Oct 11, 2024

Hi!
If you don't mind, I'd like to drag this nifty example into the guides.
It's not actually a bug. The certificate resource doesn't have any modifiable parameter that signaled the need to change.
To make your example work, simply change the resource section to the following:

resourcerouteros_system_certificate” “server_cert” {
  name = “server”
  common_name = tls_cert_request.server_csr.subject[0].common_name
  import {
    cert_file_name = routeros_file.server_cert.name
    key_file_name = routeros_file.server_key.name
  }
  depends_on = [routeros_file.server_cert, routeros_file.server_key]
  lifecycle {
    replace_triggered_by = [
      tls_locally_signed_cert.server_cert.cert_pem
    ]
  }
}

@vaerh
Copy link
Collaborator

vaerh commented Oct 11, 2024

And yes, thanks for the perfect description of the problem :)

@satwell
Copy link
Author

satwell commented Oct 11, 2024

Thanks for the quick reply! You're right, using replace_triggered_by does seem to solve the issue. It would be helpful to have that included in the example in the docs for routeros_system_certificate. (And feel free to reuse anything from my repro case to improve the docs!)

I wonder though, would it make more sense for routeros_system_certificate to allow the user to specify the content of the cert and key rather than a filename? The resource itself could be responsible for writing that content to temporary files as part of the import. It seems like this would remove the need for a lifecycle config.

And it would also fix another minor annoyance: the certificate import removes the files, so then Terraform recreates those files on the next run even if there are no other changes. So right now my example in the bug description takes two apply runs to reach steady state.

@vaerh
Copy link
Collaborator

vaerh commented Oct 11, 2024

The contents of the key and certificate can be specified in the scenario. An example is available in the documentation.
But as for creating temporary files, I would not like to do it, so as not to complicate the code.
Although it may be worth looking into this direction.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
bug Something isn't working
Projects
None yet
Development

No branches or pull requests

2 participants