Skip to content

Commit 0858940

Browse files
authored
New Provider security option and Security Practices guide (#363)
* 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
1 parent 82ec9a7 commit 0858940

File tree

6 files changed

+169
-25
lines changed

6 files changed

+169
-25
lines changed

docs/guides/security.md

Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
---
2+
layout: "heroku"
3+
page_title: "Heroku: Secure Practices"
4+
sidebar_current: "docs-heroku-guides-security"
5+
description: |-
6+
Guide to using the provider securely.
7+
---
8+
9+
# Authentication
10+
11+
The API key used by Terraform must inherently have complete permission
12+
to manage Heroku resources.
13+
14+
To generate API keys with minimal scope, see
15+
[Dev Center article **Using Terraform with Heroku: Authorization**](https://devcenter.heroku.com/articles/using-terraform-with-heroku#authorization).
16+
17+
The API key can be set for the provider following the
18+
[Provider Authentication docs](../#authentication).
19+
20+
# Sensitivity
21+
22+
Terraform includes the concept of `sensitive` values which are
23+
automatically redacted from terminal output, such as plan diffs and
24+
output summaries.
25+
26+
Various resource attributes are defined in the provider as sensitive,
27+
including: `heroku_app`#`all_config_vars`,
28+
`heroku_addon`#`config_var_values`, & `heroku_app_webhook`#`secret`.
29+
30+
In every configuration, practice marking `sensitive = true` variables &
31+
outputs that contain secret data:
32+
33+
```hcl
34+
variable "heroku_api_key" {
35+
type = string
36+
sensitive = true
37+
}
38+
39+
output "production_database_url" {
40+
type = string
41+
value = heroku_addon.production_postgres.config_var_values["DATABASE_URL"]
42+
sensitive = true
43+
}
44+
```
45+
46+
# Config Vars
47+
48+
Especially sensitive Heroku app config vars may be managed from outside of
49+
Terraform, set through `heroku config` CLI, web dashboard, or Platform API,
50+
to avoid their values touching Terraform workflows.
51+
52+
Also, config vars automatically set by add-ons, such as Postgres
53+
`DATABASE_URL`, will be recorded in Terraform state as part of the standard
54+
functionality of this Terraform provider.
55+
56+
In high-security situations, these externally managed config vars can be
57+
completely excluded from Terraform by setting the
58+
[provider attributes](../#argument-reference):
59+
60+
```hcl
61+
provider "heroku" {
62+
customizations {
63+
set_app_all_config_vars_in_state = false
64+
set_addon_config_vars_in_state = false
65+
}
66+
}
67+
```
68+
69+
As a result, `heroku_app`#`all_config_vars` and
70+
`heroku_addon`#`config_var_values` will be empty for all resources
71+
managed in Terraform.
72+
73+
# Logging
74+
75+
In normal runtime, the provider is designed to avoid logging sensitive data.
76+
77+
When `TF_LOG` environment variable is set, such as `TF_LOG=debug`, the
78+
provider will log extensive data including Heroku API calls. `Authorization`
79+
headers are automatically redacted, but logged request and response JSON
80+
bodies will contain secret values, such as app config vars.
81+
82+
Only set `TF_LOG` in environments where the sensitive log output is
83+
acceptable. Destroy/delete such logs after use to avoid disclosure.

docs/index.md

Lines changed: 14 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ simplest path to delivering apps quickly:
2323
## Guides
2424

2525
* [Upgrading](guides/upgrading.html)
26+
* [Secure Practices](guides/security.html)
2627

2728
## Contributing
2829

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

68+
🔐 See [Secure Practices](guides/security.html#authentication) for help creating a safe API token.
69+
6770
⛔️ Direct username-password authentication is [no longer supported by Heroku API](https://devcenter.heroku.com/changelog-items/2516).
6871

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

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

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

139142
* `set_app_all_config_vars_in_state` - (Optional) Controls whether the `heroku_app.all_config_vars` attribute
140-
is set in the state file. The aforementioned attribute stores a snapshot of all config vars in Terraform state,
141-
even if they are not defined in Terraform. This means sensitive Heroku add-on config vars,
142-
such as Postgres' `DATABASE_URL`, are always accessible in the state.
143-
Set to `false` to only track managed config vars in the state. Defaults to `true`.
143+
is set in the state file. Normally a snapshot of all config vars is stored in state, even though they are
144+
not managed by Terraform, such as secrets set via `heroku config` CLI, web dashboard, or add-ons like
145+
Postgres' `DATABASE_URL`. Set to `false` to only track managed config vars in the state. Defaults to `true`.
146+
See also [Secure Practices](guides/security.html).
147+
148+
* `set_addon_config_vars_in_state` - (Optional) Controls whether the `heroku_addon.config_var_values` attribute
149+
is set in the state file. The attribute stores each addon's config vars in Terraform state. This means
150+
sensitive add-on config vars, such as Postgres' `DATABASE_URL`, are always accessible in the state.
151+
Set to `false` to prevent capturing these values. Defaults to `true`.
152+
See also [Secure Practices](guides/security.html).
144153

145154
* `delays` - (Optional) Delays help mitigate issues that can arise due to
146155
Heroku's eventually consistent data model. Only a single `delays` block may be

heroku/config.go

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ const (
2525

2626
// Default custom timeouts
2727
DefaultAddonCreateTimeout = int64(20)
28+
DefaultSetAddonConfigVarsInState = true
2829
DefaultSetAppAllConfigVarsInState = true
2930
)
3031

@@ -45,6 +46,7 @@ type Config struct {
4546
AddonCreateTimeout int64
4647

4748
// Customization
49+
SetAddonConfigVarsInState bool
4850
SetAppAllConfigVarsInState bool
4951
}
5052

@@ -60,6 +62,7 @@ func NewConfig() *Config {
6062
PostDomainCreateDelay: DefaultPostDomainCreateDelay,
6163
PostSpaceCreateDelay: DefaultPostSpaceCreateDelay,
6264
AddonCreateTimeout: DefaultAddonCreateTimeout,
65+
SetAddonConfigVarsInState: DefaultSetAddonConfigVarsInState,
6366
SetAppAllConfigVarsInState: DefaultSetAppAllConfigVarsInState,
6467
}
6568
if logging.IsDebugOrHigher() {
@@ -121,6 +124,9 @@ func (c *Config) applySchema(d *schema.ResourceData) (err error) {
121124
if v, ok := customizations["set_app_all_config_vars_in_state"].(bool); ok {
122125
c.SetAppAllConfigVarsInState = v
123126
}
127+
if v, ok := customizations["set_addon_config_vars_in_state"].(bool); ok {
128+
c.SetAddonConfigVarsInState = v
129+
}
124130
}
125131
}
126132

heroku/provider.go

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@ func Provider() *schema.Provider {
3838

3939
"customizations": {
4040
Type: schema.TypeList,
41-
MaxItems: 1,
41+
MaxItems: 2,
4242
Optional: true,
4343
Elem: &schema.Resource{
4444
Schema: map[string]*schema.Schema{
@@ -47,6 +47,11 @@ func Provider() *schema.Provider {
4747
Optional: true,
4848
Default: DefaultSetAppAllConfigVarsInState,
4949
},
50+
"set_addon_config_vars_in_state": {
51+
Type: schema.TypeBool,
52+
Optional: true,
53+
Default: DefaultSetAddonConfigVarsInState,
54+
},
5055
},
5156
},
5257
},

heroku/resource_heroku_addon.go

Lines changed: 25 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -161,26 +161,29 @@ func resourceHerokuAddonCreate(d *schema.ResourceData, meta interface{}) error {
161161
d.SetId(addon.ID)
162162
log.Printf("[INFO] Addon ID: %s", d.Id())
163163

164-
err = resource.Retry(d.Timeout(schema.TimeoutCreate), func() *resource.RetryError {
165-
configVarValues, err := retrieveSpecificConfigVars(client, addon.App.ID, addon.ConfigVars)
164+
if config.SetAddonConfigVarsInState {
165+
err = resource.Retry(d.Timeout(schema.TimeoutCreate), func() *resource.RetryError {
166+
configVarValues, err := retrieveSpecificConfigVars(client, addon.App.ID, addon.ConfigVars)
167+
if err != nil {
168+
return resource.NonRetryableError(err)
169+
}
170+
if len(configVarValues) != len(addon.ConfigVars) {
171+
return resource.RetryableError(fmt.Errorf("Got %d add-on config vars from the app, but expected %d", len(configVarValues), len(addon.ConfigVars)))
172+
}
173+
log.Printf("[INFO] Addon config vars are set: %v", addon.ConfigVars)
174+
return nil
175+
})
166176
if err != nil {
167-
return resource.NonRetryableError(err)
177+
return err
168178
}
169-
if len(configVarValues) != len(addon.ConfigVars) {
170-
return resource.RetryableError(fmt.Errorf("Got %d add-on config vars from the app, but expected %d", len(configVarValues), len(addon.ConfigVars)))
171-
}
172-
log.Printf("[INFO] Addon config vars are set: %v", addon.ConfigVars)
173-
return nil
174-
})
175-
if err != nil {
176-
return err
177179
}
178180

179181
return resourceHerokuAddonRead(d, meta)
180182
}
181183

182184
func resourceHerokuAddonRead(d *schema.ResourceData, meta interface{}) error {
183-
client := meta.(*Config).Api
185+
config := meta.(*Config)
186+
client := config.Api
184187

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

211-
configVarValues, err := retrieveSpecificConfigVars(client, addon.App.ID, addon.ConfigVars)
212-
if err != nil {
213-
return err
214-
}
215-
err = d.Set("config_var_values", configVarValues)
216-
if err != nil {
217-
return err
214+
d.Set("config_var_values", map[string]string{})
215+
if config.SetAddonConfigVarsInState {
216+
configVarValues, err := retrieveSpecificConfigVars(client, addon.App.ID, addon.ConfigVars)
217+
if err != nil {
218+
return err
219+
}
220+
err = d.Set("config_var_values", configVarValues)
221+
if err != nil {
222+
return err
223+
}
218224
}
219225

220226
return nil

heroku/resource_heroku_addon_test.go

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -93,6 +93,22 @@ func TestAccHerokuAddon_ConfigVarValues(t *testing.T) {
9393
})
9494
}
9595

96+
func TestAccHerokuAddon_DontSetConfigVarValues(t *testing.T) {
97+
appName := fmt.Sprintf("tftest-%s", acctest.RandString(10))
98+
99+
resource.Test(t, resource.TestCase{
100+
PreCheck: func() { testAccPreCheck(t) },
101+
ProviderFactories: testAccProviderFactories,
102+
Steps: []resource.TestStep{
103+
{
104+
Config: testAccCheckHerokuAddonConfig_dontSetConfigVarValues(appName),
105+
Check: resource.TestCheckNoResourceAttr(
106+
"heroku_addon.pg", "config_var_values.DATABASE_URL"),
107+
},
108+
},
109+
})
110+
}
111+
96112
func TestAccHerokuAddon_CustomName(t *testing.T) {
97113
var addon heroku.AddOn
98114
appName := fmt.Sprintf("tftest-%s", acctest.RandString(10))
@@ -313,6 +329,25 @@ resource "heroku_addon" "pg" {
313329
}`, appName)
314330
}
315331

332+
func testAccCheckHerokuAddonConfig_dontSetConfigVarValues(appName string) string {
333+
return fmt.Sprintf(`
334+
provider "heroku" {
335+
customizations {
336+
set_addon_config_vars_in_state = false
337+
}
338+
}
339+
340+
resource "heroku_app" "foobar" {
341+
name = "%s"
342+
region = "us"
343+
}
344+
345+
resource "heroku_addon" "pg" {
346+
app_id = heroku_app.foobar.id
347+
plan = "heroku-postgresql:mini"
348+
}`, appName)
349+
}
350+
316351
func testAccCheckHerokuAddonConfig_no_plan(appName string) string {
317352
return fmt.Sprintf(`
318353
resource "heroku_app" "foobar" {

0 commit comments

Comments
 (0)