From f52ccb249ce4c81fa6fa5e1be6cc24fd62fce7af Mon Sep 17 00:00:00 2001 From: Joshua Spence Date: Thu, 13 Apr 2017 13:01:54 +1000 Subject: [PATCH] Hash MySQL passwords Instead of storing MySQL passwords in plaintext, hash them first. This implementation is largely based on #12128. --- mysql/resource_user.go | 30 +++++++++++++--- mysql/resource_user_test.go | 60 ++++++++++++++++++++++++++----- mysql/utils.go | 10 ++++++ website/docs/r/user.html.markdown | 19 ++++++---- 4 files changed, 101 insertions(+), 18 deletions(-) create mode 100644 mysql/utils.go diff --git a/mysql/resource_user.go b/mysql/resource_user.go index ce9bec11..46a8c914 100644 --- a/mysql/resource_user.go +++ b/mysql/resource_user.go @@ -30,10 +30,18 @@ func resourceUser() *schema.Resource { Default: "localhost", }, - "password": &schema.Schema{ + "plaintext_password": &schema.Schema{ Type: schema.TypeString, Optional: true, Sensitive: true, + StateFunc: hashSum, + }, + "password": &schema.Schema{ + Type: schema.TypeString, + Optional: true, + ConflictsWith: []string{"plaintext_password"}, + Sensitive: true, + Deprecated: "Please use plaintext_password instead", }, }, } @@ -46,7 +54,13 @@ func CreateUser(d *schema.ResourceData, meta interface{}) error { d.Get("user").(string), d.Get("host").(string)) - password := d.Get("password").(string) + var password string + if v, ok := d.GetOk("plaintext_password"); ok { + password = v.(string) + } else { + password = d.Get("password").(string) + } + if password != "" { stmtSQL = stmtSQL + fmt.Sprintf(" IDENTIFIED BY '%s'", password) } @@ -66,8 +80,16 @@ func CreateUser(d *schema.ResourceData, meta interface{}) error { func UpdateUser(d *schema.ResourceData, meta interface{}) error { conf := meta.(*providerConfiguration) - if d.HasChange("password") { - _, newpw := d.GetChange("password") + var newpw interface{} + if d.HasChange("plaintext_password") { + _, newpw = d.GetChange("plaintext_password") + } else if d.HasChange("password") { + _, newpw = d.GetChange("password") + } else { + newpw = nil + } + + if newpw != nil { var stmtSQL string /* ALTER USER syntax introduced in MySQL 5.7.6 deprecates SET PASSWORD (GH-8230) */ diff --git a/mysql/resource_user_test.go b/mysql/resource_user_test.go index a9cdd354..8f9f74b4 100644 --- a/mysql/resource_user_test.go +++ b/mysql/resource_user_test.go @@ -9,7 +9,7 @@ import ( "github.com/hashicorp/terraform/terraform" ) -func TestAccUser(t *testing.T) { +func TestAccUser_basic(t *testing.T) { resource.Test(t, resource.TestCase{ PreCheck: func() { testAccPreCheck(t) }, Providers: testAccProviders, @@ -21,11 +21,39 @@ func TestAccUser(t *testing.T) { testAccUserExists("mysql_user.test"), resource.TestCheckResourceAttr("mysql_user.test", "user", "jdoe"), resource.TestCheckResourceAttr("mysql_user.test", "host", "example.com"), - resource.TestCheckResourceAttr("mysql_user.test", "password", "password"), + resource.TestCheckResourceAttr("mysql_user.test", "plaintext_password", hashSum("password")), ), }, resource.TestStep{ Config: testAccUserConfig_newPass, + Check: resource.ComposeTestCheckFunc( + testAccUserExists("mysql_user.test"), + resource.TestCheckResourceAttr("mysql_user.test", "user", "jdoe"), + resource.TestCheckResourceAttr("mysql_user.test", "host", "example.com"), + resource.TestCheckResourceAttr("mysql_user.test", "plaintext_password", hashSum("password2")), + ), + }, + }, + }) +} + +func TestAccUser_deprecated(t *testing.T) { + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + CheckDestroy: testAccUserCheckDestroy, + Steps: []resource.TestStep{ + resource.TestStep{ + Config: testAccUserConfig_deprecated, + Check: resource.ComposeTestCheckFunc( + testAccUserExists("mysql_user.test"), + resource.TestCheckResourceAttr("mysql_user.test", "user", "jdoe"), + resource.TestCheckResourceAttr("mysql_user.test", "host", "example.com"), + resource.TestCheckResourceAttr("mysql_user.test", "password", "password"), + ), + }, + resource.TestStep{ + Config: testAccUserConfig_deprecated_newPass, Check: resource.ComposeTestCheckFunc( testAccUserExists("mysql_user.test"), resource.TestCheckResourceAttr("mysql_user.test", "user", "jdoe"), @@ -86,16 +114,32 @@ func testAccUserCheckDestroy(s *terraform.State) error { const testAccUserConfig_basic = ` resource "mysql_user" "test" { - user = "jdoe" - host = "example.com" - password = "password" + user = "jdoe" + host = "example.com" + plaintext_password = "password" } ` const testAccUserConfig_newPass = ` resource "mysql_user" "test" { - user = "jdoe" - host = "example.com" - password = "password2" + user = "jdoe" + host = "example.com" + plaintext_password = "password2" +} +` + +const testAccUserConfig_deprecated = ` +resource "mysql_user" "test" { + user = "jdoe" + host = "example.com" + password = "password" +} +` + +const testAccUserConfig_deprecated_newPass = ` +resource "mysql_user" "test" { + user = "jdoe" + host = "example.com" + password = "password2" } ` diff --git a/mysql/utils.go b/mysql/utils.go new file mode 100644 index 00000000..f5a46b26 --- /dev/null +++ b/mysql/utils.go @@ -0,0 +1,10 @@ +package mysql + +import ( + "crypto/sha256" + "fmt" +) + +func hashSum(contents interface{}) string { + return fmt.Sprintf("%x", sha256.Sum256([]byte(contents.(string)))) +} diff --git a/website/docs/r/user.html.markdown b/website/docs/r/user.html.markdown index 6f3f8b50..1811fb5c 100644 --- a/website/docs/r/user.html.markdown +++ b/website/docs/r/user.html.markdown @@ -11,16 +11,18 @@ description: |- The ``mysql_user`` resource creates and manages a user on a MySQL server. -~> **Note:** All arguments including username and password will be stored in the raw state as plain-text. +~> **Note:** The password for the user is provided in plain text, and is +obscured by an unsalted hash in the state [Read more about sensitive data in state](/docs/state/sensitive-data.html). +Care is required when using this resource, to avoid disclosing the password. ## Example Usage ```hcl resource "mysql_user" "jdoe" { - user = "jdoe" - host = "example.com" - password = "password" + user = "jdoe" + host = "example.com" + plaintext_password = "password" } ``` @@ -32,8 +34,13 @@ The following arguments are supported: * `host` - (Optional) The source host of the user. Defaults to "localhost". -* `password` - (Optional) The password of the user. The value of this - argument is plain-text so make sure to secure where this is defined. +* `plaintext_password` - (Optional) The password for the user. This must be + provided in plain text, so the data source for it must be secured. + An _unsalted_ hash of the provided password is stored in state. + +* `password` - (Optional) Deprecated alias of `plaintext_password`, whose + value is *stored as plaintext in state*. Prefer to use `plaintext_password` + instead, which stores the password as an unsalted hash. ## Attributes Reference