<?xml version="1.0" encoding="utf-8" standalone="yes"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
  <channel>
    <title>Aws on 404 Code Not Found</title>
    <link>https://www.404-code-not-found.com/tags/aws/</link>
    <description>Recent content in Aws on 404 Code Not Found</description>
    <generator>Hugo</generator>
    <language>en-us</language>
    <lastBuildDate>Thu, 10 Sep 2026 09:00:00 -0600</lastBuildDate>
    <atom:link href="https://www.404-code-not-found.com/tags/aws/index.xml" rel="self" type="application/rss+xml"/>
    <item>
      <title>Stop Minting Static Credentials</title>
      <link>https://www.404-code-not-found.com/posts/stop-minting-static-credentials/</link>
      <pubDate>Thu, 10 Sep 2026 09:00:00 -0600</pubDate>
      <guid>https://www.404-code-not-found.com/posts/stop-minting-static-credentials/</guid>
      <category>terraform</category>
      <category>vault</category>
      <category>security</category>
      <category>oidc</category>
      <category>aws</category>
      <category>azure</category>
      <category>gcp</category>
      <category>postgresql</category>
      <description>&lt;p&gt;&lt;img src=&#34;https://www.404-code-not-found.com/posts/stop-minting-static-credentials/header.png&#34; alt=&#34;Stop Minting Static Credentials&#34;&gt;&lt;/p&gt;&#xA;&lt;h2 id=&#34;introduction&#34;&gt;Introduction&lt;/h2&gt;&#xA;&lt;p&gt;Every static credential starts out reasonable. A pipeline needs to SSH into a&#xA;server, so someone generates a key pair and stores the private half somewhere&#xA;the pipeline can read it. A Terraform workspace needs to talk to AWS, so someone&#xA;creates an access key and pastes it into a workspace variable. An application&#xA;needs a database, so someone creates an application user, picks a password, and&#xA;puts it in a config file.&lt;/p&gt;&#xA;&lt;p&gt;Each of those is a credential that nobody asked to expire. It works on day one,&#xA;and it keeps working long after the person who created it has moved on and&#xA;nobody remembers which systems would break if it were rotated. So it doesn&amp;rsquo;t&#xA;get rotated.&lt;/p&gt;&#xA;&lt;p&gt;The fix has the same shape every time. Stop storing a secret that proves who you&#xA;are, and start trusting an identity system to issue a short-lived one on demand.&#xA;This post walks through three of those substitutions, each done for real in my&#xA;lab environment, and the thing that bit on the way in for each one:&lt;/p&gt;&#xA;&lt;ol&gt;&#xA;&lt;li&gt;A Vault SSH certificate authority instead of distributing key pairs.&lt;/li&gt;&#xA;&lt;li&gt;OIDC workload identity instead of long-lived cloud access keys, for AWS,&#xA;Azure, and Google Cloud.&lt;/li&gt;&#xA;&lt;li&gt;Vault-managed database credentials instead of a shared application user.&lt;/li&gt;&#xA;&lt;/ol&gt;&#xA;&lt;p&gt;The Azure and Google Cloud trust configurations are in&#xA;&lt;a href=&#34;https://github.com/404-code-not-found-com/terraform-dynamic-credentials&#34; target=&#34;_blank&#34;&gt;terraform-dynamic-credentials&lt;/a&gt;,&#xA;verified against both HCP Terraform and a self-hosted Terraform Enterprise.&#xA;AWS dynamic credentials worked in my lab too, but an account admin created&#xA;the IAM side, and I don&amp;rsquo;t have admin access to an AWS account to show every&#xA;step here. The AWS module in the repo is written from the HashiCorp docs and&#xA;passes &lt;code&gt;terraform validate&lt;/code&gt;, but I haven&amp;rsquo;t applied it myself. Code excerpts&#xA;elsewhere in the post are trimmed, with environment-specific names generalized.&lt;/p&gt;&#xA;&lt;h2 id=&#34;tldr&#34;&gt;TL;DR&lt;/h2&gt;&#xA;&lt;ul&gt;&#xA;&lt;li&gt;&lt;strong&gt;SSH:&lt;/strong&gt; Vault&amp;rsquo;s SSH secrets engine signs a certificate per job, good for&#xA;thirty minutes. Hosts trust the CA&amp;rsquo;s public key, baked into the image. There&#xA;is no private key to distribute because there is no long-lived private key&#xA;that grants access.&lt;/li&gt;&#xA;&lt;li&gt;&lt;strong&gt;Cloud:&lt;/strong&gt; HCP Terraform and Terraform Enterprise sign a JWT for every plan&#xA;and apply. AWS, Azure, and Google Cloud trade it for credentials that expire&#xA;with the run. The workspace stores identifiers, not secrets.&lt;/li&gt;&#xA;&lt;li&gt;&lt;strong&gt;Databases:&lt;/strong&gt; a Vault static role owns the application user&amp;rsquo;s password and&#xA;rotates it on a schedule. A dynamic role goes further and creates a new user&#xA;per lease. Both work, and they fail in different ways.&lt;/li&gt;&#xA;&lt;li&gt;The gotchas are almost never in the happy path. They&amp;rsquo;re in the second phase,&#xA;the second week, or the second time you need to change something.&lt;/li&gt;&#xA;&lt;/ul&gt;&#xA;&lt;h2 id=&#34;problem-1-the-ssh-key-pair-everyone-has-a-copy-of&#34;&gt;Problem 1: The SSH Key Pair Everyone Has a Copy Of&lt;/h2&gt;&#xA;&lt;p&gt;My lab&amp;rsquo;s CI pipeline needed to configure a lab server over SSH. The original&#xA;setup was the common one: a private key stored in Vault&amp;rsquo;s key/value engine, read&#xA;by the CI job, written to disk, and used directly.&lt;/p&gt;&#xA;&lt;div class=&#34;highlight&#34;&gt;&lt;pre&gt;&lt;code class=&#34;language-yaml&#34; data-lang=&#34;yaml&#34;&gt;secrets:&#xA;  SSH_PRIVATE_KEY:&#xA;    vault:&#xA;      engine:&#xA;        name: kv-v2&#xA;        path: secrets&#xA;      path: lab/lab-key&#xA;      field: private_key&#xA;    file: true&#xA;    token: $VAULT_ID_TOKEN&#xA;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Storing the key in Vault beats storing it in the repo, but it doesn&amp;rsquo;t change&#xA;what the key is. It&amp;rsquo;s one private key, valid indefinitely, copied onto every&#xA;runner that executes the job, and rotating it means touching every host that&#xA;trusts it.&lt;/p&gt;&#xA;&lt;h3 id=&#34;the-fix-sign-a-certificate-per-job&#34;&gt;The Fix: Sign a Certificate per Job&lt;/h3&gt;&#xA;&lt;p&gt;OpenSSH has supported certificates for years. Instead of putting a user&amp;rsquo;s public&#xA;key in &lt;code&gt;authorized_keys&lt;/code&gt; on every host, you put one CA public key in&#xA;&lt;code&gt;sshd_config&lt;/code&gt;, and any user certificate signed by that CA is accepted until it&#xA;expires. Vault&amp;rsquo;s SSH secrets engine is that CA.&lt;/p&gt;&#xA;&lt;p&gt;The Vault side is a mount, a CA, and a role that says what a certificate may&#xA;contain:&lt;/p&gt;&#xA;&lt;div class=&#34;highlight&#34;&gt;&lt;pre&gt;&lt;code class=&#34;language-hcl&#34; data-lang=&#34;hcl&#34;&gt;resource &amp;#34;vault_mount&amp;#34; &amp;#34;aap_ssh&amp;#34; {&#xA;  path = &amp;#34;aap-ssh&amp;#34;&#xA;  type = &amp;#34;ssh&amp;#34;&#xA;}&#xA;&#xA;# generate_signing_key keeps the CA private key inside Vault. It is never&#xA;# returned by the API, so it can&amp;#39;t land in Terraform state.&#xA;resource &amp;#34;vault_ssh_secret_backend_ca&amp;#34; &amp;#34;aap_ssh&amp;#34; {&#xA;  backend              = vault_mount.aap_ssh.path&#xA;  generate_signing_key = true&#xA;  key_type             = &amp;#34;ed25519&amp;#34;&#xA;}&#xA;&#xA;resource &amp;#34;vault_ssh_secret_backend_role&amp;#34; &amp;#34;aap_role&amp;#34; {&#xA;  name    = &amp;#34;aap-role&amp;#34;&#xA;  backend = vault_mount.aap_ssh.path&#xA;&#xA;  key_type                = &amp;#34;ca&amp;#34;&#xA;  allow_user_certificates = true&#xA;&#xA;  allowed_users = &amp;#34;ansible&amp;#34;&#xA;  default_user  = &amp;#34;ansible&amp;#34;&#xA;&#xA;  allowed_extensions = &amp;#34;permit-pty,permit-port-forwarding&amp;#34;&#xA;  default_extensions = {&#xA;    &amp;#34;permit-pty&amp;#34; = &amp;#34;&amp;#34;&#xA;  }&#xA;&#xA;  ttl                 = &amp;#34;1800&amp;#34; # 30m&#xA;  max_ttl             = &amp;#34;7200&amp;#34; # 2h&#xA;  not_before_duration = &amp;#34;30&amp;#34;&#xA;}&#xA;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;The host side lives in the image pipeline. CI reads the CA&amp;rsquo;s public key from&#xA;Vault, and Packer drops it into the image along with one line of &lt;code&gt;sshd_config&lt;/code&gt;:&lt;/p&gt;&#xA;&lt;div class=&#34;highlight&#34;&gt;&lt;pre&gt;&lt;code class=&#34;language-bash&#34; data-lang=&#34;bash&#34;&gt;CA_DEST=/etc/ssh/vault-ssh-ca.pub&#xA;cp /tmp/vault-ssh-ca.pub &amp;#34;$CA_DEST&amp;#34;&#xA;chmod 0644 &amp;#34;$CA_DEST&amp;#34;&#xA;&#xA;if grep -q &amp;#39;^TrustedUserCAKeys&amp;#39; /etc/ssh/sshd_config; then&#xA;  sed -i &amp;#34;s|^TrustedUserCAKeys .*|TrustedUserCAKeys $CA_DEST|&amp;#34; /etc/ssh/sshd_config&#xA;else&#xA;  echo &amp;#34;TrustedUserCAKeys $CA_DEST&amp;#34; &amp;gt;&amp;gt; /etc/ssh/sshd_config&#xA;fi&#xA;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;On the client side, the lab server is now configured by Ansible Automation&#xA;Platform rather than by the CI job. AAP has a &lt;code&gt;HashiCorp Vault Signed SSH&lt;/code&gt;&#xA;credential type, and linking it to a Machine credential makes AAP request a&#xA;freshly signed certificate every time a job runs:&lt;/p&gt;&#xA;&lt;div class=&#34;highlight&#34;&gt;&lt;pre&gt;&lt;code class=&#34;language-yaml&#34; data-lang=&#34;yaml&#34;&gt;- name: Ensure signed-SSH input source on Machine credential&#xA;  ansible.controller.credential_input_source:&#xA;    input_field_name: ssh_public_key_data&#xA;    target_credential: &amp;#34;{{ machine_credential_name }}&amp;#34;&#xA;    source_credential: &amp;#34;{{ signed_ssh_lookup_name }}&amp;#34;&#xA;    metadata:&#xA;      secret_path: aap-ssh&#xA;      role: aap-role&#xA;      auth_path: approle&#xA;      valid_principals: ansible&#xA;      public_key: &amp;#34;{{ ssh_public_key }}&amp;#34;&#xA;    state: present&#xA;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;The end state: the lab&amp;rsquo;s instances launch with no EC2 key pair at all, port 22&#xA;only accepts connections from AAP&amp;rsquo;s egress range, and the static key is gone&#xA;from the pipeline. The key pair AAP signs still exists, but on&#xA;its own it opens nothing. A certificate is what gets you in, and each one is&#xA;dead within two hours at most.&lt;/p&gt;&#xA;&lt;h3 id=&#34;gotcha-the-ca-is-not-a-normal-resource&#34;&gt;Gotcha: The CA Is Not a Normal Resource&lt;/h3&gt;&#xA;&lt;p&gt;I originally built the CA by hand, as RSA 4096, and brought it under Terraform&#xA;later. That&amp;rsquo;s where the first surprise was. Every argument on&#xA;&lt;code&gt;vault_ssh_secret_backend_ca&lt;/code&gt; forces replacement, because the provider&#xA;implements create, read, and delete but no update. And the provider&amp;rsquo;s read only&#xA;returns &lt;code&gt;public_key&lt;/code&gt; and &lt;code&gt;backend&lt;/code&gt;, so after an &lt;code&gt;import&lt;/code&gt; the other arguments are&#xA;null in state. A configuration that matched the live CA exactly still planned a&#xA;replacement.&lt;/p&gt;&#xA;&lt;p&gt;There was no way to adopt the existing CA without regenerating it. So instead of&#xA;importing it as RSA and rotating to ed25519 later, which would have regenerated&#xA;it twice, I changed the key type in the same apply and regenerated it once.&lt;/p&gt;&#xA;&lt;p&gt;Any change to the CA resource produces a new CA, and every host trusting the old&#xA;public key stops accepting certificates. In a setup where the key is baked into&#xA;images, rotation isn&amp;rsquo;t a Vault operation. It&amp;rsquo;s&#xA;a Vault change followed by rebuilding every image. &lt;code&gt;generate_signing_key = true&lt;/code&gt;&#xA;makes this sharper: the private key never leaves Vault, which is exactly why it&#xA;isn&amp;rsquo;t in state, and also why losing Vault means a new CA and a full image&#xA;rebuild. I think that&amp;rsquo;s the right trade, but it should be a decision, not a&#xA;discovery.&lt;/p&gt;&#xA;&lt;h3 id=&#34;gotcha-write-the-ttls-in-seconds&#34;&gt;Gotcha: Write the TTLs in Seconds&lt;/h3&gt;&#xA;&lt;p&gt;The first version of the role said &lt;code&gt;ttl = &amp;quot;30m&amp;quot;&lt;/code&gt;. It applied cleanly, and then&#xA;every plan after it showed a change. The API returns TTLs as integers in string&#xA;form, the provider doesn&amp;rsquo;t normalize them, and &lt;code&gt;&amp;quot;30m&amp;quot;&lt;/code&gt; is not equal to &lt;code&gt;&amp;quot;1800&amp;quot;&lt;/code&gt;.&#xA;Writing seconds made the plan idempotent.&lt;/p&gt;&#xA;&lt;h3 id=&#34;gotcha-the-public-key-has-a-space-in-it&#34;&gt;Gotcha: The Public Key Has a Space in It&lt;/h3&gt;&#xA;&lt;p&gt;The first image pipeline passed the CA public key to Packer&amp;rsquo;s shell provisioner&#xA;as an environment variable, with this &lt;code&gt;execute_command&lt;/code&gt;:&lt;/p&gt;&#xA;&lt;div class=&#34;highlight&#34;&gt;&lt;pre&gt;&lt;code class=&#34;language-hcl&#34; data-lang=&#34;hcl&#34;&gt;execute_command  = &amp;#34;sudo sh -c &amp;#39;{{ .Vars }} {{ .Path }}&amp;#39;&amp;#34;&#xA;environment_vars = [&amp;#34;VAULT_SSH_CA_PUBKEY=${var.vault_ssh_ca_pubkey}&amp;#34;]&#xA;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;An SSH public key is &lt;code&gt;&amp;lt;type&amp;gt; &amp;lt;key&amp;gt;&lt;/code&gt;, with a space in the middle, and &lt;code&gt;{{ .Vars }}&lt;/code&gt;&#xA;expands inside the single-quoted &lt;code&gt;sh -c&lt;/code&gt; string. The quoting broke.&lt;/p&gt;&#xA;&lt;p&gt;The fix was to never let the key pass through a shell at all. GitLab CI can&#xA;write a Vault secret to a temporary file instead of an environment variable:&lt;/p&gt;&#xA;&lt;div class=&#34;highlight&#34;&gt;&lt;pre&gt;&lt;code class=&#34;language-yaml&#34; data-lang=&#34;yaml&#34;&gt;VAULT_SSH_CA_PUBKEY:&#xA;  vault:&#xA;    engine:&#xA;      name: generic&#xA;      path: aap-ssh&#xA;    path: config/ca&#xA;    field: public_key&#xA;  file: true&#xA;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Packer then uploads that file with a &lt;code&gt;file&lt;/code&gt; provisioner, and the script reads it&#xA;from disk. The key never goes through an environment variable, a command-line&#xA;argument, or a shell expansion.&lt;/p&gt;&#xA;&lt;h2 id=&#34;problem-2-cloud-keys-in-workspace-variables&#34;&gt;Problem 2: Cloud Keys in Workspace Variables&lt;/h2&gt;&#xA;&lt;p&gt;The classic Terraform setup for AWS is an IAM user with an access key, pasted&#xA;into the workspace as &lt;code&gt;AWS_ACCESS_KEY_ID&lt;/code&gt; and &lt;code&gt;AWS_SECRET_ACCESS_KEY&lt;/code&gt;. It works,&#xA;it never expires, and it&amp;rsquo;s usually more privileged than anything else in the&#xA;account.&lt;/p&gt;&#xA;&lt;p&gt;My lab had already moved past that, or so I thought. The workspace&#xA;authenticated to Vault with workload identity, then read AWS credentials from&#xA;Vault&amp;rsquo;s AWS secrets engine, which created a fresh IAM user for each run:&lt;/p&gt;&#xA;&lt;div class=&#34;highlight&#34;&gt;&lt;pre&gt;&lt;code class=&#34;language-hcl&#34; data-lang=&#34;hcl&#34;&gt;data &amp;#34;vault_aws_access_credentials&amp;#34; &amp;#34;creds&amp;#34; {&#xA;  backend = &amp;#34;aws&amp;#34;&#xA;  role    = &amp;#34;packer&amp;#34;&#xA;}&#xA;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;On paper, those are dynamic credentials. In practice, the first AWS call failed&#xA;about 23 seconds after the credentials were minted, with&#xA;&lt;code&gt;403 InvalidClientTokenId&lt;/code&gt; from STS. Adding &lt;code&gt;skip_credentials_validation&lt;/code&gt; moved&#xA;the same 403 to &lt;code&gt;iam:ListRoles&lt;/code&gt;. Adding &lt;code&gt;skip_requesting_account_id&lt;/code&gt; and a&#xA;30-second &lt;code&gt;time_sleep&lt;/code&gt; moved it again, this time to a &lt;code&gt;401 AuthFailure&lt;/code&gt; from the&#xA;first EC2 call, 56 seconds after minting. The error kept moving later instead of&#xA;going away. A brand new IAM user takes a while to be recognized everywhere in&#xA;AWS, and there was no sleep long enough to fix that reliably.&lt;/p&gt;&#xA;&lt;p&gt;The HashiCorp docs say not to do exactly this: &amp;ldquo;data sources that&#xA;use secrets engines to generate dynamic secrets must not be used with Vault&#xA;dynamic credentials.&amp;rdquo; The documented route through Vault is Vault-backed dynamic&#xA;credentials, which even has a &lt;code&gt;TFC_VAULT_BACKED_AWS_SLEEP_SECONDS&lt;/code&gt; setting &amp;ldquo;to&#xA;mitigate eventual consistency issues in AWS when using the &lt;code&gt;iam_user&lt;/code&gt; auth&#xA;type.&amp;rdquo;&lt;/p&gt;&#xA;&lt;p&gt;I went the other way and dropped Vault out of the AWS path entirely. With the&#xA;platform&amp;rsquo;s native dynamic credentials, the same apply created a key pair, a&#xA;security group, an EC2 instance, and an Elastic IP with no delay at all.&lt;/p&gt;&#xA;&lt;h3 id=&#34;the-fix-let-the-cloud-trust-the-platform&#34;&gt;The Fix: Let the Cloud Trust the Platform&lt;/h3&gt;&#xA;&lt;p&gt;HCP Terraform and Terraform Enterprise act as an OIDC identity provider. For&#xA;every plan and every apply, the platform signs a JWT describing the run. The&#xA;claim that matters is &lt;code&gt;sub&lt;/code&gt;:&lt;/p&gt;&#xA;&lt;div class=&#34;highlight&#34;&gt;&lt;pre&gt;&lt;code class=&#34;language-text&#34; data-lang=&#34;text&#34;&gt;organization:my-org:project:my-project:workspace:my-workspace:run_phase:apply&#xA;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;You configure the cloud to trust tokens from the platform&amp;rsquo;s issuer URL, and to&#xA;accept only tokens whose claims match your workspace. At run time, the provider&#xA;trades the token for short-lived cloud credentials. The workspace holds a role&#xA;ARN or a client ID, which are identifiers rather than secrets.&lt;/p&gt;&#xA;&lt;p&gt;The docs are blunt about what happens if you skip the matching: &amp;ldquo;If you don&amp;rsquo;t&#xA;match against at least the organization name, any organization or workspace on&#xA;HCP Terraform will be able to access your cloud resources!&amp;rdquo;&lt;/p&gt;&#xA;&lt;p&gt;Something still needs admin rights to create the trust in the first place. The&#xA;difference is that it happens once, from a person&amp;rsquo;s own login, rather than being&#xA;stored in every workspace.&lt;/p&gt;&#xA;&lt;h3 id=&#34;aws&#34;&gt;AWS&lt;/h3&gt;&#xA;&lt;p&gt;The trust side is an IAM OIDC provider for the platform&amp;rsquo;s hostname and a role&#xA;whose trust policy checks the audience and the subject:&lt;/p&gt;&#xA;&lt;div class=&#34;highlight&#34;&gt;&lt;pre&gt;&lt;code class=&#34;language-hcl&#34; data-lang=&#34;hcl&#34;&gt;resource &amp;#34;aws_iam_role&amp;#34; &amp;#34;this&amp;#34; {&#xA;  name = var.name&#xA;&#xA;  assume_role_policy = jsonencode({&#xA;    Version = &amp;#34;2012-10-17&amp;#34;&#xA;    Statement = [{&#xA;      Effect    = &amp;#34;Allow&amp;#34;&#xA;      Principal = { Federated = local.oidc_provider_arn }&#xA;      Action    = &amp;#34;sts:AssumeRoleWithWebIdentity&amp;#34;&#xA;      Condition = {&#xA;        StringEquals = {&#xA;          &amp;#34;${var.hostname}:aud&amp;#34; = &amp;#34;aws.workload.identity&amp;#34;&#xA;        }&#xA;        StringLike = {&#xA;          &amp;#34;${var.hostname}:sub&amp;#34; = &amp;#34;organization:${var.organization}:project:${var.project}:workspace:${var.workspace}:run_phase:*&amp;#34;&#xA;        }&#xA;      }&#xA;    }]&#xA;  })&#xA;}&#xA;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Note &lt;code&gt;StringLike&lt;/code&gt; on the subject. The &lt;code&gt;*&lt;/code&gt; after &lt;code&gt;run_phase:&lt;/code&gt; is only a wildcard&#xA;under &lt;code&gt;StringLike&lt;/code&gt;, and the docs call this out explicitly for when plan and&#xA;apply share a role. The workspace then needs two environment variables,&#xA;&lt;code&gt;TFC_AWS_PROVIDER_AUTH=true&lt;/code&gt; and &lt;code&gt;TFC_AWS_RUN_ROLE_ARN&lt;/code&gt;, and the provider block&#xA;should contain nothing but &lt;code&gt;region&lt;/code&gt;.&lt;/p&gt;&#xA;&lt;p&gt;In my lab, I specified the role and an account admin created it, so I can&amp;rsquo;t&#xA;publish the live trust policy as tested. The module in the repo is written from&#xA;the docs and passes &lt;code&gt;terraform validate&lt;/code&gt;, but I haven&amp;rsquo;t applied it myself.&#xA;Azure and Google Cloud I built and ran end to end.&lt;/p&gt;&#xA;&lt;h3 id=&#34;azure&#34;&gt;Azure&lt;/h3&gt;&#xA;&lt;p&gt;Azure&amp;rsquo;s version is an app registration, its service principal, a role&#xA;assignment, and federated identity credentials that tell Entra ID which tokens&#xA;to accept:&lt;/p&gt;&#xA;&lt;div class=&#34;highlight&#34;&gt;&lt;pre&gt;&lt;code class=&#34;language-hcl&#34; data-lang=&#34;hcl&#34;&gt;resource &amp;#34;azuread_application_federated_identity_credential&amp;#34; &amp;#34;this&amp;#34; {&#xA;  for_each = toset([&amp;#34;plan&amp;#34;, &amp;#34;apply&amp;#34;])&#xA;&#xA;  application_id = azuread_application.this.id&#xA;  audiences      = [&amp;#34;api://AzureADTokenExchange&amp;#34;]&#xA;  display_name   = &amp;#34;${var.name}-${each.key}&amp;#34;&#xA;  issuer         = &amp;#34;https://${var.hostname}&amp;#34;&#xA;  subject        = &amp;#34;organization:${var.organization}:project:${var.project}:workspace:${var.workspace}:run_phase:${each.key}&amp;#34;&#xA;}&#xA;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;The workspace gets &lt;code&gt;TFC_AZURE_PROVIDER_AUTH=true&lt;/code&gt;, &lt;code&gt;TFC_AZURE_RUN_CLIENT_ID&lt;/code&gt;,&#xA;&lt;code&gt;ARM_SUBSCRIPTION_ID&lt;/code&gt;, and &lt;code&gt;ARM_TENANT_ID&lt;/code&gt;.&lt;/p&gt;&#xA;&lt;p&gt;The gotcha is the &lt;code&gt;for_each&lt;/code&gt;. A federated credential&amp;rsquo;s subject is an exact&#xA;string match, no wildcards, and the token&amp;rsquo;s subject ends in the run phase. So&#xA;every workspace needs two credentials. I deleted the &lt;code&gt;apply&lt;/code&gt; one to see what&#xA;happens: the plan ran fine, because data sources authenticate during plan, and&#xA;then the apply failed:&lt;/p&gt;&#xA;&lt;div class=&#34;highlight&#34;&gt;&lt;pre&gt;&lt;code class=&#34;language-text&#34; data-lang=&#34;text&#34;&gt;AADSTS700213: No matching federated identity record found for presented&#xA;assertion subject &amp;#39;organization:my-org:project:dynamic-credentials-demo:workspace:dynamic-credentials-demo:run_phase:apply&amp;#39;.&#xA;Check your federated identity credential Subject, Audience and Issuer against&#xA;the presented assertion.&#xA;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;That&amp;rsquo;s a nasty failure mode, because a green plan is what gets approved. Combine it with Entra ID&amp;rsquo;s limit of 20 federated credentials per app&#xA;registration, and one app registration can serve at most ten workspaces.&lt;/p&gt;&#xA;&lt;p&gt;Microsoft has a preview feature, flexible federated identity credentials, that&#xA;allows wildcard matching on the subject. I tested it both ways. On HCP&#xA;Terraform, a single credential matching&#xA;&lt;code&gt;claims[&#39;sub&#39;] matches &#39;...:run_phase:*&#39;&lt;/code&gt; covered both plan and apply. On&#xA;Terraform Enterprise, Entra ID refused to create it:&lt;/p&gt;&#xA;&lt;div class=&#34;highlight&#34;&gt;&lt;pre&gt;&lt;code class=&#34;language-text&#34; data-lang=&#34;text&#34;&gt;Expression is not supported for applications in this cloud &amp;#39;Public&amp;#39; using&#xA;issuer &amp;#39;https://&amp;lt;tfe-hostname&amp;gt;&amp;#39;.&#xA;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Microsoft&amp;rsquo;s docs list only &lt;code&gt;https://app.terraform.io&lt;/code&gt; and its EU equivalent as&#xA;supported Terraform issuers. If you run Terraform Enterprise, two credentials&#xA;per workspace is the design.&lt;/p&gt;&#xA;&lt;h3 id=&#34;google-cloud&#34;&gt;Google Cloud&lt;/h3&gt;&#xA;&lt;p&gt;Google Cloud uses a workload identity pool, an OIDC provider in that pool, and a&#xA;service account the workspace impersonates:&lt;/p&gt;&#xA;&lt;div class=&#34;highlight&#34;&gt;&lt;pre&gt;&lt;code class=&#34;language-hcl&#34; data-lang=&#34;hcl&#34;&gt;resource &amp;#34;google_iam_workload_identity_pool_provider&amp;#34; &amp;#34;this&amp;#34; {&#xA;  project                            = var.project_id&#xA;  workload_identity_pool_id          = google_iam_workload_identity_pool.this.workload_identity_pool_id&#xA;  workload_identity_pool_provider_id = var.pool_provider_id&#xA;&#xA;  attribute_mapping = {&#xA;    &amp;#34;google.subject&amp;#34;                        = &amp;#34;assertion.sub&amp;#34;&#xA;    &amp;#34;attribute.terraform_organization_name&amp;#34; = &amp;#34;assertion.terraform_organization_name&amp;#34;&#xA;    &amp;#34;attribute.terraform_project_name&amp;#34;      = &amp;#34;assertion.terraform_project_name&amp;#34;&#xA;    &amp;#34;attribute.terraform_workspace_name&amp;#34;    = &amp;#34;assertion.terraform_workspace_name&amp;#34;&#xA;    &amp;#34;attribute.terraform_run_phase&amp;#34;         = &amp;#34;assertion.terraform_run_phase&amp;#34;&#xA;  }&#xA;&#xA;  attribute_condition = &amp;#34;assertion.sub.startsWith(\&amp;#34;organization:${var.organization}:project:${var.project}:workspace:${var.workspace}:\&amp;#34;)&amp;#34;&#xA;&#xA;  oidc {&#xA;    issuer_uri = &amp;#34;https://${var.hostname}&amp;#34;&#xA;  }&#xA;}&#xA;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;That &lt;code&gt;startsWith&lt;/code&gt; condition covers both run phases, so Google Cloud doesn&amp;rsquo;t have&#xA;Azure&amp;rsquo;s two-credential problem. It carries more weight than it looks like it&#xA;does, though. The service account binding grants &lt;code&gt;roles/iam.workloadIdentityUser&lt;/code&gt;&#xA;to every identity in the pool:&lt;/p&gt;&#xA;&lt;div class=&#34;highlight&#34;&gt;&lt;pre&gt;&lt;code class=&#34;language-hcl&#34; data-lang=&#34;hcl&#34;&gt;member = &amp;#34;principalSet://iam.googleapis.com/${google_iam_workload_identity_pool.this.name}/*&amp;#34;&#xA;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;So the condition is the only thing checking that the token came from your&#xA;workspace and not from someone else&amp;rsquo;s. When I changed it to a different&#xA;organization, the plan failed immediately:&lt;/p&gt;&#xA;&lt;div class=&#34;highlight&#34;&gt;&lt;pre&gt;&lt;code class=&#34;language-text&#34; data-lang=&#34;text&#34;&gt;oauth2/google: status code 400: {&amp;#34;error&amp;#34;:&amp;#34;unauthorized_client&amp;#34;,&#xA;&amp;#34;error_description&amp;#34;:&amp;#34;The given credential is rejected by the attribute condition.&amp;#34;}&#xA;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;The workspace gets &lt;code&gt;TFC_GCP_PROVIDER_AUTH=true&lt;/code&gt;,&#xA;&lt;code&gt;TFC_GCP_WORKLOAD_PROVIDER_NAME&lt;/code&gt;, and &lt;code&gt;TFC_GCP_RUN_SERVICE_ACCOUNT_EMAIL&lt;/code&gt;.&lt;/p&gt;&#xA;&lt;h3 id=&#34;if-you-run-terraform-enterprise&#34;&gt;If You Run Terraform Enterprise&lt;/h3&gt;&#xA;&lt;p&gt;Everything above works the same on Terraform Enterprise, with the issuer set to&#xA;your own hostname instead of &lt;code&gt;app.terraform.io&lt;/code&gt;. Two things change.&lt;/p&gt;&#xA;&lt;p&gt;First, the cloud has to be able to reach you. To verify a token, the cloud&#xA;fetches &lt;code&gt;/.well-known/openid-configuration&lt;/code&gt; and &lt;code&gt;/.well-known/jwks&lt;/code&gt; from your&#xA;Terraform Enterprise hostname. An instance that&amp;rsquo;s only reachable on a private&#xA;network can&amp;rsquo;t do native dynamic credentials to a public cloud. Vault-backed&#xA;dynamic credentials exist partly for this case.&lt;/p&gt;&#xA;&lt;p&gt;Second, the certificate. HashiCorp&amp;rsquo;s Azure docs say &amp;ldquo;Custom and self-signed&#xA;certificates are not supported due to restrictions in Azure,&amp;rdquo; and the Google&#xA;Cloud docs say dynamic credentials &amp;ldquo;do not work if your Terraform Enterprise&#xA;instance uses a custom or self-signed certificate.&amp;rdquo; My instance uses a publicly&#xA;trusted certificate, so neither applied, but an instance behind an internal CA&#xA;would have hit both.&lt;/p&gt;&#xA;&lt;h2 id=&#34;problem-3-the-shared-application-user&#34;&gt;Problem 3: The Shared Application User&lt;/h2&gt;&#xA;&lt;p&gt;The database version of a static credential is an application user whose&#xA;password was set once, lives in a config file or a Kubernetes secret, and is&#xA;shared by every instance of the application. Vault&amp;rsquo;s database secrets engine&#xA;offers two ways out, and my lab has both configured against the same Postgres&#xA;database for Terraform Enterprise.&lt;/p&gt;&#xA;&lt;h3 id=&#34;the-fix-part-one-a-static-role&#34;&gt;The Fix, Part One: A Static Role&lt;/h3&gt;&#xA;&lt;p&gt;A static role is a one-to-one mapping between a Vault role and an existing&#xA;database user. Vault stores the user&amp;rsquo;s password and rotates it on a schedule.&#xA;Terraform Enterprise itself runs as &lt;code&gt;tfe_app&lt;/code&gt;, whose password Vault rotates&#xA;weekly:&lt;/p&gt;&#xA;&lt;div class=&#34;highlight&#34;&gt;&lt;pre&gt;&lt;code class=&#34;language-hcl&#34; data-lang=&#34;hcl&#34;&gt;resource &amp;#34;vault_database_secret_backend_static_role&amp;#34; &amp;#34;tfe_static&amp;#34; {&#xA;  backend         = vault_mount.tfe_psql.path&#xA;  name            = &amp;#34;tfe-static&amp;#34;&#xA;  db_name         = &amp;#34;tfe-psql&amp;#34;&#xA;  username        = &amp;#34;tfe_app&amp;#34;&#xA;  rotation_period = 604800 # 7 days&#xA;}&#xA;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Two things to know before you create one. Vault rotates the password the&#xA;moment the role is created, so the password you created the user with stops&#xA;working immediately. And the Vault docs are explicit that the user Vault itself&#xA;connects as should never be a static role: &amp;ldquo;Do not manage the same root database&#xA;credentials that you provide to Vault in config/ with static roles.&amp;rdquo; In my lab,&#xA;Vault connects as &lt;code&gt;tfe&lt;/code&gt;, and the application runs as &lt;code&gt;tfe_app&lt;/code&gt;.&lt;/p&gt;&#xA;&lt;p&gt;Terraform Enterprise runs on Kubernetes, so the Vault Secrets Operator (VSO)&#xA;delivers the password as a Kubernetes secret and restarts the deployment when it&#xA;changes:&lt;/p&gt;&#xA;&lt;div class=&#34;highlight&#34;&gt;&lt;pre&gt;&lt;code class=&#34;language-yaml&#34; data-lang=&#34;yaml&#34;&gt;apiVersion: secrets.hashicorp.com/v1beta1&#xA;kind: VaultDynamicSecret&#xA;metadata:&#xA;  name: tfe-database&#xA;spec:&#xA;  vaultAuthRef: tfe-auth&#xA;  mount: tfe-psql&#xA;  path: static-creds/tfe-static&#xA;  allowStaticCreds: true&#xA;  refreshAfter: 24h&#xA;  destination:&#xA;    name: tfe-database&#xA;    create: true&#xA;  rolloutRestartTargets:&#xA;    - kind: Deployment&#xA;      name: terraform-enterprise&#xA;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h3 id=&#34;gotcha-the-first-rotation-is-the-real-test&#34;&gt;Gotcha: The First Rotation Is the Real Test&lt;/h3&gt;&#xA;&lt;p&gt;The first version of that manifest didn&amp;rsquo;t have &lt;code&gt;allowStaticCreds&lt;/code&gt;. It deployed&#xA;fine and Terraform Enterprise ran fine, for a week. Then Vault rotated the&#xA;password on schedule, the Kubernetes secret still held the old one, and Terraform&#xA;Enterprise started logging:&lt;/p&gt;&#xA;&lt;div class=&#34;highlight&#34;&gt;&lt;pre&gt;&lt;code class=&#34;language-text&#34; data-lang=&#34;text&#34;&gt;failed SASL auth: FATAL: password authentication failed for user &amp;#34;tfe_app&amp;#34; (SQLSTATE 28P01)&#xA;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Without &lt;code&gt;allowStaticCreds&lt;/code&gt;, VSO 1.4.0 treated the static credential response as&#xA;a non-renewable lease, concluded there was nothing left to refresh, and never&#xA;read it again. Its log said as much: &amp;ldquo;Vault secret does not support periodic&#xA;renewal/refresh via reconciliation.&amp;rdquo; The VSO API reference describes the flag as&#xA;something that &amp;ldquo;should be set when syncing credentials that are periodically&#xA;rotated by the Vault server, rather than created upon request.&amp;rdquo;&lt;/p&gt;&#xA;&lt;p&gt;A rotation setup that hasn&amp;rsquo;t survived its first rotation isn&amp;rsquo;t tested yet. If&#xA;your rotation period is a week, you find out a week later, which is usually&#xA;long after the change that caused it has been merged and forgotten.&lt;/p&gt;&#xA;&lt;h3 id=&#34;the-fix-part-two-a-dynamic-role&#34;&gt;The Fix, Part Two: A Dynamic Role&lt;/h3&gt;&#xA;&lt;p&gt;A dynamic role goes further. Instead of rotating one user&amp;rsquo;s password, Vault&#xA;creates a brand new database user for each lease and drops it when the lease&#xA;ends. Every instance of the application gets its own username, which the Vault&#xA;docs point out makes auditing easier, because you can trace a query to a&#xA;specific instance.&lt;/p&gt;&#xA;&lt;p&gt;My lab has a dynamic role on the same database:&lt;/p&gt;&#xA;&lt;div class=&#34;highlight&#34;&gt;&lt;pre&gt;&lt;code class=&#34;language-hcl&#34; data-lang=&#34;hcl&#34;&gt;resource &amp;#34;vault_database_secret_backend_role&amp;#34; &amp;#34;tfe_psql&amp;#34; {&#xA;  backend = vault_mount.tfe_psql.path&#xA;  name    = &amp;#34;tfe-psql-role&amp;#34;&#xA;  db_name = &amp;#34;tfe-psql&amp;#34;&#xA;&#xA;  creation_statements = [&#xA;    &amp;#34;CREATE ROLE \&amp;#34;{{name}}\&amp;#34; WITH LOGIN PASSWORD &amp;#39;{{password}}&amp;#39; VALID UNTIL &amp;#39;{{expiration}}&amp;#39;;\nGRANT ALL PRIVILEGES ON DATABASE tfe TO \&amp;#34;{{name}}\&amp;#34;;&amp;#34;&#xA;  ]&#xA;&#xA;  default_ttl = 3600  # 1 hour&#xA;  max_ttl     = 86400 # 24 hours&#xA;}&#xA;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Nothing in the lab actually consumes it. It has only ever been read by hand. So&#xA;before writing about it, I reproduced it against a throwaway Vault and Postgres&#xA;17, with the same creation statement and a table owned by a separate user, and&#xA;tried to use it the way an application would.&lt;/p&gt;&#xA;&lt;h3 id=&#34;gotcha-grant-all-on-a-database-doesnt-reach-the-tables&#34;&gt;Gotcha: GRANT ALL on a Database Doesn&amp;rsquo;t Reach the Tables&lt;/h3&gt;&#xA;&lt;p&gt;The generated user could log in, and that was all:&lt;/p&gt;&#xA;&lt;div class=&#34;highlight&#34;&gt;&lt;pre&gt;&lt;code class=&#34;language-text&#34; data-lang=&#34;text&#34;&gt;ERROR:  permission denied for table widgets&#xA;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;In Postgres, the privileges on a database are about connecting to it and&#xA;creating things in it, not about the tables inside it. &lt;code&gt;GRANT ALL PRIVILEGES ON DATABASE&lt;/code&gt; reads like it grants everything, and it grants none of the access an&#xA;application needs. The example role in Vault&amp;rsquo;s own Postgres docs uses&#xA;&lt;code&gt;GRANT SELECT ON ALL TABLES IN SCHEMA public&lt;/code&gt; instead.&lt;/p&gt;&#xA;&lt;h3 id=&#34;gotcha-a-user-that-owns-something-cant-be-dropped&#34;&gt;Gotcha: A User That Owns Something Can&amp;rsquo;t Be Dropped&lt;/h3&gt;&#xA;&lt;p&gt;The second problem is worse, because it shows up when the lease ends. I granted&#xA;the dynamic user permission to create tables, had it create one, and revoked the&#xA;lease. Vault queued the revocation, and the user was still there. The Vault log&#xA;explained why:&lt;/p&gt;&#xA;&lt;div class=&#34;highlight&#34;&gt;&lt;pre&gt;&lt;code class=&#34;language-text&#34; data-lang=&#34;text&#34;&gt;[ERROR] expiration: failed to revoke lease: lease_id=db/creds/dyn/h3AscHe4Ki5S9HC096qXdMsL&#xA;error=&amp;#34;failed to revoke entry: resp: (*logical.Response)(nil) err: ERROR: role&#xA;\&amp;#34;v-token-dyn-hOoD3GJA9yCYjC7PX4uC-1789066250\&amp;#34; cannot be dropped because some&#xA;objects depend on it (SQLSTATE 2BP01)&amp;#34; attempts=1 next_attempt=21.012025154s&#xA;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Vault retries, and it keeps failing. Any application that runs its own schema&#xA;migrations will create objects owned by whichever user ran them, which is a&#xA;user that&amp;rsquo;s supposed to disappear.&lt;/p&gt;&#xA;&lt;p&gt;The fix that worked was to give ownership to a role that never logs in, and&#xA;make every dynamic user act as that role:&lt;/p&gt;&#xA;&lt;div class=&#34;highlight&#34;&gt;&lt;pre&gt;&lt;code class=&#34;language-sql&#34; data-lang=&#34;sql&#34;&gt;CREATE ROLE app_schema NOLOGIN;&#xA;ALTER TABLE widgets OWNER TO app_schema;&#xA;GRANT CREATE ON SCHEMA public TO app_schema;&#xA;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Then the creation statement makes each generated user a member of &lt;code&gt;app_schema&lt;/code&gt;&#xA;and sets it as their default role:&lt;/p&gt;&#xA;&lt;div class=&#34;highlight&#34;&gt;&lt;pre&gt;&lt;code class=&#34;language-sql&#34; data-lang=&#34;sql&#34;&gt;CREATE ROLE &amp;#34;{{name}}&amp;#34; WITH LOGIN PASSWORD &amp;#39;{{password}}&amp;#39;&#xA;  VALID UNTIL &amp;#39;{{expiration}}&amp;#39; IN ROLE app_schema;&#xA;ALTER ROLE &amp;#34;{{name}}&amp;#34; SET ROLE app_schema;&#xA;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;With that in place, the generated user could read the table, a table it created&#xA;was owned by &lt;code&gt;app_schema&lt;/code&gt;, and revoking the lease dropped the user cleanly.&lt;/p&gt;&#xA;&lt;h3 id=&#34;static-or-dynamic&#34;&gt;Static or Dynamic?&lt;/h3&gt;&#xA;&lt;p&gt;They fail differently, and that&amp;rsquo;s the useful way to choose. A static role keeps&#xA;one username and changes its password, so the risk is a consumer that doesn&amp;rsquo;t&#xA;pick up the new one. A dynamic role changes the username every lease, so the&#xA;risk is anything tied to the old username: permissions granted to it, and&#xA;objects it owns.&lt;/p&gt;&#xA;&lt;p&gt;Terraform Enterprise in my lab uses the static role. &lt;code&gt;tfe_app&lt;/code&gt; owns the &lt;code&gt;tfe&lt;/code&gt;&#xA;database and its &lt;code&gt;public&lt;/code&gt; schema, so the ownership problem never comes up, and&#xA;the only thing that has to be right is the rotation. For an application that&#xA;doesn&amp;rsquo;t own its schema, or where each instance should have its own identity,&#xA;a dynamic role is the stronger option, as long as its creation statement grants&#xA;access to the tables and not just the database.&lt;/p&gt;&#xA;&lt;h2 id=&#34;conclusion&#34;&gt;Conclusion&lt;/h2&gt;&#xA;&lt;p&gt;Every substitution here follows the same pattern: remove the stored secret,&#xA;trust an identity system instead, and make whatever it issues expire.&lt;/p&gt;&#xA;&lt;ul&gt;&#xA;&lt;li&gt;&lt;strong&gt;SSH certificates instead of key pairs&lt;/strong&gt; - Vault signs a certificate per job&#xA;with a thirty-minute TTL, and hosts trust the CA through &lt;code&gt;TrustedUserCAKeys&lt;/code&gt;.&#xA;Treat the CA resource as immutable: changing it means rebuilding every image&#xA;that trusts it.&lt;/li&gt;&#xA;&lt;li&gt;&lt;strong&gt;Workload identity instead of access keys&lt;/strong&gt; - the platform signs a token per&#xA;run phase, and the cloud trades it for credentials that expire with the run.&#xA;On AWS, use &lt;code&gt;StringLike&lt;/code&gt; for the wildcard. On Azure, create a federated&#xA;credential for plan &lt;em&gt;and&lt;/em&gt; apply. On Google Cloud, the attribute condition is&#xA;the only check that the token came from your workspace.&lt;/li&gt;&#xA;&lt;li&gt;&lt;strong&gt;Terraform Enterprise changes two things&lt;/strong&gt; - the cloud has to reach its OIDC&#xA;endpoints, and Azure and Google Cloud won&amp;rsquo;t accept a custom or self-signed&#xA;certificate.&lt;/li&gt;&#xA;&lt;li&gt;&lt;strong&gt;Vault-owned database credentials instead of a shared user&lt;/strong&gt; - static roles&#xA;rotate one user&amp;rsquo;s password, and nothing proves the delivery works until the&#xA;first rotation. Dynamic roles create a user per lease, and need a creation&#xA;statement that grants table access and keeps ownership off the ephemeral user.&lt;/li&gt;&#xA;&lt;li&gt;&lt;strong&gt;Test past the first run&lt;/strong&gt; - none of these failures showed up there. The&#xA;Azure apply failed after a clean plan, the VSO outage waited a week for the&#xA;first rotation, and the stuck revocation only appeared when the lease ended.&lt;/li&gt;&#xA;&lt;/ul&gt;&#xA;&lt;p&gt;The Azure and Google Cloud configurations, with a workload you can run against&#xA;your own HCP Terraform or Terraform Enterprise workspace, are in&#xA;&lt;a href=&#34;https://github.com/404-code-not-found-com/terraform-dynamic-credentials&#34; target=&#34;_blank&#34;&gt;terraform-dynamic-credentials&lt;/a&gt;.&lt;/p&gt;&#xA;</description>
    </item>
  </channel>
</rss>
