Skip to content

Commit

Permalink
New Provider security option and Security Practices guide (#363)
Browse files Browse the repository at this point in the history
* Provider configuration to skip setting addon config_var_values in state (matching the prior functionality for app all_config_vars)

* New Guide: Security Practices, including how the provider customizations for security work
  • Loading branch information
mars authored Mar 15, 2023
1 parent 82ec9a7 commit 0858940
Show file tree
Hide file tree
Showing 6 changed files with 169 additions and 25 deletions.
83 changes: 83 additions & 0 deletions docs/guides/security.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
---
layout: "heroku"
page_title: "Heroku: Secure Practices"
sidebar_current: "docs-heroku-guides-security"
description: |-
Guide to using the provider securely.
---

# Authentication

The API key used by Terraform must inherently have complete permission
to manage Heroku resources.

To generate API keys with minimal scope, see
[Dev Center article **Using Terraform with Heroku: Authorization**](https://devcenter.heroku.com/articles/using-terraform-with-heroku#authorization).

The API key can be set for the provider following the
[Provider Authentication docs](../#authentication).

# Sensitivity

Terraform includes the concept of `sensitive` values which are
automatically redacted from terminal output, such as plan diffs and
output summaries.

Various resource attributes are defined in the provider as sensitive,
including: `heroku_app`#`all_config_vars`,
`heroku_addon`#`config_var_values`, & `heroku_app_webhook`#`secret`.

In every configuration, practice marking `sensitive = true` variables &
outputs that contain secret data:

```hcl
variable "heroku_api_key" {
type = string
sensitive = true
}
output "production_database_url" {
type = string
value = heroku_addon.production_postgres.config_var_values["DATABASE_URL"]
sensitive = true
}
```

# Config Vars

Especially sensitive Heroku app config vars may be managed from outside of
Terraform, set through `heroku config` CLI, web dashboard, or Platform API,
to avoid their values touching Terraform workflows.

Also, config vars automatically set by add-ons, such as Postgres
`DATABASE_URL`, will be recorded in Terraform state as part of the standard
functionality of this Terraform provider.

In high-security situations, these externally managed config vars can be
completely excluded from Terraform by setting the
[provider attributes](../#argument-reference):

```hcl
provider "heroku" {
customizations {
set_app_all_config_vars_in_state = false
set_addon_config_vars_in_state = false
}
}
```

As a result, `heroku_app`#`all_config_vars` and
`heroku_addon`#`config_var_values` will be empty for all resources
managed in Terraform.

# Logging

In normal runtime, the provider is designed to avoid logging sensitive data.

When `TF_LOG` environment variable is set, such as `TF_LOG=debug`, the
provider will log extensive data including Heroku API calls. `Authorization`
headers are automatically redacted, but logged request and response JSON
bodies will contain secret values, such as app config vars.

Only set `TF_LOG` in environments where the sensitive log output is
acceptable. Destroy/delete such logs after use to avoid disclosure.
19 changes: 14 additions & 5 deletions docs/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ simplest path to delivering apps quickly:
## Guides

* [Upgrading](guides/upgrading.html)
* [Secure Practices](guides/security.html)

## Contributing

Expand Down Expand Up @@ -64,6 +65,8 @@ All authentication tokens must be generated with one of these methods:
* `heroku auth` command of the [Heroku CLI](https://devcenter.heroku.com/articles/heroku-cli)
* [Heroku Platform APIs: OAuth](https://devcenter.heroku.com/articles/platform-api-reference#oauth-authorization)

🔐 See [Secure Practices](guides/security.html#authentication) for help creating a safe API token.

⛔️ Direct username-password authentication is [no longer supported by Heroku API](https://devcenter.heroku.com/changelog-items/2516).

### Static credentials
Expand Down Expand Up @@ -123,7 +126,7 @@ The directory containing the `.netrc` file can be overridden by the `NETRC` envi
The following arguments are supported:

* `api_key` - (Required) Heroku API token. It must be provided, but it can also
be sourced from [other locations](#Authentication).
be sourced from [other locations](#Authentication). See also [Secure Practices](guides/security.html).

* `email` - (Ignored) This field originally supported username-password authentication,
but has since [been deprecated](https://devcenter.heroku.com/changelog-items/2516).
Expand All @@ -137,10 +140,16 @@ The following arguments are supported:
Only a single `customizations` block may be specified, and it supports the following arguments:

* `set_app_all_config_vars_in_state` - (Optional) Controls whether the `heroku_app.all_config_vars` attribute
is set in the state file. The aforementioned attribute stores a snapshot of all config vars in Terraform state,
even if they are not defined in Terraform. This means sensitive Heroku add-on config vars,
such as Postgres' `DATABASE_URL`, are always accessible in the state.
Set to `false` to only track managed config vars in the state. Defaults to `true`.
is set in the state file. Normally a snapshot of all config vars is stored in state, even though they are
not managed by Terraform, such as secrets set via `heroku config` CLI, web dashboard, or add-ons like
Postgres' `DATABASE_URL`. Set to `false` to only track managed config vars in the state. Defaults to `true`.
See also [Secure Practices](guides/security.html).

* `set_addon_config_vars_in_state` - (Optional) Controls whether the `heroku_addon.config_var_values` attribute
is set in the state file. The attribute stores each addon's config vars in Terraform state. This means
sensitive add-on config vars, such as Postgres' `DATABASE_URL`, are always accessible in the state.
Set to `false` to prevent capturing these values. Defaults to `true`.
See also [Secure Practices](guides/security.html).

* `delays` - (Optional) Delays help mitigate issues that can arise due to
Heroku's eventually consistent data model. Only a single `delays` block may be
Expand Down
6 changes: 6 additions & 0 deletions heroku/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ const (

// Default custom timeouts
DefaultAddonCreateTimeout = int64(20)
DefaultSetAddonConfigVarsInState = true
DefaultSetAppAllConfigVarsInState = true
)

Expand All @@ -45,6 +46,7 @@ type Config struct {
AddonCreateTimeout int64

// Customization
SetAddonConfigVarsInState bool
SetAppAllConfigVarsInState bool
}

Expand All @@ -60,6 +62,7 @@ func NewConfig() *Config {
PostDomainCreateDelay: DefaultPostDomainCreateDelay,
PostSpaceCreateDelay: DefaultPostSpaceCreateDelay,
AddonCreateTimeout: DefaultAddonCreateTimeout,
SetAddonConfigVarsInState: DefaultSetAddonConfigVarsInState,
SetAppAllConfigVarsInState: DefaultSetAppAllConfigVarsInState,
}
if logging.IsDebugOrHigher() {
Expand Down Expand Up @@ -121,6 +124,9 @@ func (c *Config) applySchema(d *schema.ResourceData) (err error) {
if v, ok := customizations["set_app_all_config_vars_in_state"].(bool); ok {
c.SetAppAllConfigVarsInState = v
}
if v, ok := customizations["set_addon_config_vars_in_state"].(bool); ok {
c.SetAddonConfigVarsInState = v
}
}
}

Expand Down
7 changes: 6 additions & 1 deletion heroku/provider.go
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ func Provider() *schema.Provider {

"customizations": {
Type: schema.TypeList,
MaxItems: 1,
MaxItems: 2,
Optional: true,
Elem: &schema.Resource{
Schema: map[string]*schema.Schema{
Expand All @@ -47,6 +47,11 @@ func Provider() *schema.Provider {
Optional: true,
Default: DefaultSetAppAllConfigVarsInState,
},
"set_addon_config_vars_in_state": {
Type: schema.TypeBool,
Optional: true,
Default: DefaultSetAddonConfigVarsInState,
},
},
},
},
Expand Down
44 changes: 25 additions & 19 deletions heroku/resource_heroku_addon.go
Original file line number Diff line number Diff line change
Expand Up @@ -161,26 +161,29 @@ func resourceHerokuAddonCreate(d *schema.ResourceData, meta interface{}) error {
d.SetId(addon.ID)
log.Printf("[INFO] Addon ID: %s", d.Id())

err = resource.Retry(d.Timeout(schema.TimeoutCreate), func() *resource.RetryError {
configVarValues, err := retrieveSpecificConfigVars(client, addon.App.ID, addon.ConfigVars)
if config.SetAddonConfigVarsInState {
err = resource.Retry(d.Timeout(schema.TimeoutCreate), func() *resource.RetryError {
configVarValues, err := retrieveSpecificConfigVars(client, addon.App.ID, addon.ConfigVars)
if err != nil {
return resource.NonRetryableError(err)
}
if len(configVarValues) != len(addon.ConfigVars) {
return resource.RetryableError(fmt.Errorf("Got %d add-on config vars from the app, but expected %d", len(configVarValues), len(addon.ConfigVars)))
}
log.Printf("[INFO] Addon config vars are set: %v", addon.ConfigVars)
return nil
})
if err != nil {
return resource.NonRetryableError(err)
return err
}
if len(configVarValues) != len(addon.ConfigVars) {
return resource.RetryableError(fmt.Errorf("Got %d add-on config vars from the app, but expected %d", len(configVarValues), len(addon.ConfigVars)))
}
log.Printf("[INFO] Addon config vars are set: %v", addon.ConfigVars)
return nil
})
if err != nil {
return err
}

return resourceHerokuAddonRead(d, meta)
}

func resourceHerokuAddonRead(d *schema.ResourceData, meta interface{}) error {
client := meta.(*Config).Api
config := meta.(*Config)
client := config.Api

addon, err := resourceHerokuAddonRetrieve(d.Id(), client)
if err != nil {
Expand Down Expand Up @@ -208,13 +211,16 @@ func resourceHerokuAddonRead(d *schema.ResourceData, meta interface{}) error {
return err
}

configVarValues, err := retrieveSpecificConfigVars(client, addon.App.ID, addon.ConfigVars)
if err != nil {
return err
}
err = d.Set("config_var_values", configVarValues)
if err != nil {
return err
d.Set("config_var_values", map[string]string{})
if config.SetAddonConfigVarsInState {
configVarValues, err := retrieveSpecificConfigVars(client, addon.App.ID, addon.ConfigVars)
if err != nil {
return err
}
err = d.Set("config_var_values", configVarValues)
if err != nil {
return err
}
}

return nil
Expand Down
35 changes: 35 additions & 0 deletions heroku/resource_heroku_addon_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,22 @@ func TestAccHerokuAddon_ConfigVarValues(t *testing.T) {
})
}

func TestAccHerokuAddon_DontSetConfigVarValues(t *testing.T) {
appName := fmt.Sprintf("tftest-%s", acctest.RandString(10))

resource.Test(t, resource.TestCase{
PreCheck: func() { testAccPreCheck(t) },
ProviderFactories: testAccProviderFactories,
Steps: []resource.TestStep{
{
Config: testAccCheckHerokuAddonConfig_dontSetConfigVarValues(appName),
Check: resource.TestCheckNoResourceAttr(
"heroku_addon.pg", "config_var_values.DATABASE_URL"),
},
},
})
}

func TestAccHerokuAddon_CustomName(t *testing.T) {
var addon heroku.AddOn
appName := fmt.Sprintf("tftest-%s", acctest.RandString(10))
Expand Down Expand Up @@ -313,6 +329,25 @@ resource "heroku_addon" "pg" {
}`, appName)
}

func testAccCheckHerokuAddonConfig_dontSetConfigVarValues(appName string) string {
return fmt.Sprintf(`
provider "heroku" {
customizations {
set_addon_config_vars_in_state = false
}
}
resource "heroku_app" "foobar" {
name = "%s"
region = "us"
}
resource "heroku_addon" "pg" {
app_id = heroku_app.foobar.id
plan = "heroku-postgresql:mini"
}`, appName)
}

func testAccCheckHerokuAddonConfig_no_plan(appName string) string {
return fmt.Sprintf(`
resource "heroku_app" "foobar" {
Expand Down

0 comments on commit 0858940

Please sign in to comment.