provider-configuration

>-

hashicorp/agent-skills275 installsMPL-2.0Synced Aug 27

Works with

Claude CodeCursorCodex CLIGitHub CopilotGemini CLI
---
name: provider-configuration
description: >-
license: MPL-2.0
---

# Terraform Provider Configuration and Authentication

How a provider accepts connection settings and resolves credentials. Poor
authentication UX is the first thing every user of a provider hits; a
well-designed credential provider chain is what separates a production-grade
provider from a demo. The examples use a fictional `examplecloud` provider
and the [Plugin Framework](https://developer.hashicorp.com/terraform/plugin/framework/providers).

**References** (load when needed):
- `references/credential-chain.md` — complete, compilable credential chain
  implementation (providers, chain, file profiles, Configure wiring, tests)
- `references/case-studies.md` — how the AWS provider (`aws-sdk-go-base`)
  and smaller providers structure real credential chains

---

## Provider Schema for Authentication

Every authentication attribute must be `Optional`, never `Required` — a
`Required` attribute forces users to put credentials in configuration and
makes environment-variable and credentials-file resolution impossible. Mark
secrets `Sensitive` so Terraform redacts them in plan output, and state the
environment-variable fallback in each description so `tfplugindocs` publishes
the resolution rules.

```go
func (p *examplecloudProvider) Schema(ctx context.Context, req provider.SchemaRequest, resp *provider.SchemaResponse) {
    resp.Schema = schema.Schema{
        Attributes: map[string]schema.Attribute{
            "endpoint": schema.StringAttribute{
                Optional:            true,
                MarkdownDescription: "API endpoint. May also be set via the `EXAMPLECLOUD_ENDPOINT` environment variable.",
            },
            "api_key": schema.StringAttribute{
                Optional:            true,
                MarkdownDescription: "API key. May also be set via the `EXAMPLECLOUD_API_KEY` environment variable, or in a shared credentials file.",
            },
            "api_secret": schema.StringAttribute{
                Optional:            true,
                Sensitive:           true,
                MarkdownDescription: "API secret. May also be set via the `EXAMPLECLOUD_API_SECRET` environment variable, or in a shared credentials file.",
            },
            "profile": schema.StringAttribute{
                Optional:            true,
                MarkdownDescription: "Named profile in the shared credentials file. May also be set via the `EXAMPLECLOUD_PROFILE` environment variable. Defaults to `default`.",
            },
            "skip_credentials_validation": schema.BoolAttribute{
                Optional:            true,
                MarkdownDescription: "Skip the identity check normally performed during provider configuration.",
            },
        },
    }
}
```

Never add a `Default` to a credential attribute, and never hardcode a
credential anywhere in the provider. Defaults belong in the resolution logic
(where environment variables and files can override them), not in the schema.

## The Credential Provider Chain

Resolve credentials by consulting an ordered list of sources and taking the
first one that produces a **complete** set. This is the pattern the AWS
provider uses via [`aws-sdk-go-base`](https://github.com/hashicorp/aws-sdk-go-base),
and it generalizes to any provider. The canonical precedence, highest first:

1. **Static configuration** — values set directly in the `provider` block.
   Explicit always wins.
2. **Environment variables** — `EXAMPLECLOUD_API_KEY`, etc. The CI-friendly
   path.
3. **Shared credentials file** — named profiles in
   `~/.examplecloud/credentials`, for humans with multiple accounts.
4. **Platform identity** — instance metadata, workload identity, or OIDC
   token exchange, where the platform offers it. Credentials nobody has to
   store.

Two rules make the chain predictable:

- **Resolve secrets as a set, not field-by-field.** If the environment
  supplies an API key but no secret, that source offers nothing — fall
  through to the next source for *both* values. Mixing an env-var key with a
  file-profile secret produces authentication failures that are nearly
  impossible for users to debug.
- **Resolve non-secret connection settings field-by-field.** `endpoint`,
  `profile`, or `insecure` can each independently follow
  config > env > file > default, because a mismatch there is visible and
  harmless.

The core abstraction is a single-method interface with a sentinel error that
distinguishes "this source has nothing to offer" (fall through) from "this
source is misconfigured" (surface it):

```go
// ErrNoCredentials signals a source had nothing to offer. The chain falls
// through to the next source. Any other error means the source was
// configured but unusable (e.g. malformed credentials file) and is
// preserved so the final diagnostics can surface it.
var ErrNoCredentials = errors.New("no credentials found")

type Credentials struct {
    APIKey    string
    APISecret string
    Source    string // which provider supplied them, for logging
}

func (c Credentials) Complete() bool {
    return c.APIKey != "" && c.APISecret != ""
}

type Provider interface {
    Retrieve(ctx context.Context) (Credentials, error)
    Name() string
}
```

A `Chain` (itself a `Provider`, so chains compose) walks the providers in
order and returns the first complete set of credentials. Every skipped
source is recorded into an aggregate `ChainError` whose `Error()` lists each
source with the reason it was skipped, and whose `Is` method makes
`errors.Is(err, ErrNoCredentials)` true only when every source fell through
cleanly — so `Configure` can tell "nothing supplied" from "something
supplied but broken" with one check. The full implementation — the chain
loop, the static, environment, and file providers, and the
`NewDefaultChain` constructor that owns the canonical order — lives in
`references/credential-chain.md`.

## Wiring the Chain into Configure

`Configure` runs once per Terraform operation, before any resource CRUD.
The shape:

```go
func (p *examplecloudProvider) Configure(ctx context.Context, req provider.ConfigureRequest, resp *provider.ConfigureResponse) {
    var config examplecloudProviderModel
    resp.Diagnostics.Append(req.Config.Get(ctx, &config)...)
    if resp.Diagnostics.HasError() {
        return
    }

    // 1. Guard against unknown values (e.g. api_key = some_resource.output).
    if config.APIKey.IsUnknown() {
        resp.Diagnostics.AddAttributeError(
            path.Root("api_key"),
            "Unknown API Key",
            "The provider cannot connect because api_key depends on a value known only after apply. "+
                "Set a static value, or use the EXAMPLECLOUD_API_KEY environment variable.",
        )
    }
    // ... repeat for each auth attribute, then:
    if resp.Diagnostics.HasError() {
        return
    }

    // 2. Resolve credentials through the chain.
    chain := credentials.NewDefaultChain(
        config.APIKey.ValueString(),
        config.APISecret.ValueString(),
        credentials.Options{Profile: config.Profile.ValueString()},
    )
    creds, err := chain.Retrieve(ctx)
    if err != nil {
        if errors.Is(err, credentials.ErrNoCredentials) {
            resp.Diagnostics.AddError(
                "No Valid Credential Sources Found",
                "No examplecloud credentials were found. Sources tried, in order:\n\n"+err.Error()+
                    "\n\nSet api_key and api_secret in the provider block, export "+
                    "EXAMPLECLOUD_API_KEY and EXAMPLECLOUD_API_SECRET, or add a profile to "+
                    "~/.examplecloud/credentials. See https://example.com/docs/auth.",
            )
        } else {
            resp.Diagnostics.AddError("Failed to Resolve Credentials", err.Error())
        }
        return
    }
    tflog.Debug(ctx, "resolved credentials", map[string]any{"source": creds.Source})

    // 3. Build the client once; share it with every resource and data source.
    client := examplecloud.NewClient(endpoint, creds.APIKey, creds.APISecret)
    resp.DataSourceData = client
    resp.ResourceData = client
}
```

Why each step matters:

- **Unknown-value guards.** During planning, an attribute wired to another
  resource's output is *unknown*, not null. Without the guard the provider
  silently treats it as empty, falls through the chain, and authenticates as
  the wrong identity — or fails with a misleading "missing credentials"
  error. Name the environment-variable workaround in the guard message.
- **The sentinel check picks the right message.** "You gave me nothing"
  (actionable list of options) is a different failure from "you gave me
  something broken" (show the parse error). Collapsing them into one message
  is how providers end up with users pasting secrets into config to debug.
- **Log the source, never the secret.** Knowing *which* source won is the
  single most useful debugging fact and costs nothing to log.

## Diagnostics That Unblock Users

An authentication error message is the provider's most-read documentation.
Every credential failure diagnostic should name:

- **Every source tried, in order, with why it was skipped** — the
  `ChainError` provides this. `aws-sdk-go-base` does the same with its
  `NoValidCredentialSourcesError`.
- **The exact environment variable names** and the credentials file path and
  profile that were consulted — not "set the appropriate environment
  variables".
- **A documentation URL** for the provider's authentication guide.

Use warnings (not errors) for conditions that are suspicious but not fatal,
naming what took precedence: a `profile` set while environment credentials
are also present (which wins?), or a credentials file with group/world-read
permissions (suggest `chmod 0600`).

## Secret Hygiene

- Give the `Credentials` type `String()` and `GoString()` methods that
  redact secret fields, so a stray `%v`, `%+v`, or error wrap can never leak
  a secret into logs or diagnostics.
- Never include credential *values* in diagnostics, log lines, or wrapped
  errors — log the source name and non-secret identifiers only.
- Warn when a credentials file is readable by other users
  (`info.Mode().Perm()&0o077 != 0`); skip this check on Windows, where POSIX
  permission bits are not meaningful.

## Configure-Time Validation

Resolve the chain eagerly in `Configure` — never lazily on first resource
use — so a credentials problem fails one time, at plan, with a good message,
instead of failing in the middle of an apply. If the API has a cheap
identity endpoint (the equivalent of AWS `sts:GetCallerIdentity` or a
`/whoami`), call it after resolving credentials so *invalid* (not just
missing) credentials also fail at configure time. Gate it behind a
`skip_credentials_validation` attribute for air-gapped or stubbed
environments.

## Unit Testing the Chain

The chain is pure logic — test it with unit tests (`Test` prefix, no
`TF_ACC`), not acceptance tests. Make the environment injectable (a
`getenv func(string) string` field defaulting to `os.Getenv`, or use
`t.Setenv`) and point the file provider at `t.TempDir()` fixtures. The
tests that matter:

- **Per-source**: each provider returns its credentials when set and
  `ErrNoCredentials` when incomplete (a key with no secret is incomplete).
- **Precedence**: static beats env; env beats file; chain falls through to
  the file when nothing above supplies a complete set.
- **Failure aggregation**: with all sources empty,
  `errors.Is(err, ErrNoCredentials)` is true and the message names every
  source.
- **Hard errors**: a malformed credentials file or an *explicitly requested*
  profile that does not exist surfaces a descriptive error rather than
  silently falling through (a merely defaulted profile falls through).
- **Redaction**: `fmt.Sprintf("%v")` and `%+v` of a `Credentials` value
  never contain the secret.

Full test examples are in `references/credential-chain.md`.

## Checklist

- [ ] All auth attributes `Optional`; secrets marked `Sensitive: true`
- [ ] Attribute descriptions name their environment-variable fallbacks
- [ ] Unknown-value guards on every auth attribute in `Configure`
- [ ] Chain precedence: static config > env vars > credentials file > platform identity
- [ ] Secrets resolved as a complete set; non-secret settings field-by-field
- [ ] Sentinel `ErrNoCredentials` distinguishes fall-through from hard failure
- [ ] Missing-credentials diagnostic lists every source tried + docs URL
- [ ] `Credentials` type redacts secrets in `String()`/`GoString()`
- [ ] Credentials-file permission warning (non-Windows)
- [ ] Eager resolution in `Configure`; optional identity check with `skip_credentials_validation`
- [ ] Unit tests cover per-source behavior, precedence, aggregation, redaction
- [ ] No credential value ever logged or embedded in an error

## Related Skills

Use the `new-terraform-provider` skill (if available) to scaffold the
provider this configuration lives in, and the `provider-resources` skill for
consuming the configured client from resources and data sources.

More General & Other skills

← All General & Other skills

Check your AI visibility

One URL in, a 0–100 score and the exact fixes out.

RUN THE CHECK

Browse all the tools

15 tools across six categories
13 of them never send your data anywhere

Free · No signup · No trial clock

SEE THE DIRECTORY