Skip to content

Commit

Permalink
feat: Add private key passphrase support (#639)
Browse files Browse the repository at this point in the history
  • Loading branch information
robbruce committed Aug 19, 2021
1 parent 965fea7 commit a1c4067
Show file tree
Hide file tree
Showing 7 changed files with 138 additions and 56 deletions.
66 changes: 45 additions & 21 deletions docs/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,22 +12,23 @@ Coverage is focused on part of Snowflake related to access control.
## Example Provider Configuration

```terraform
provider snowflake {
provider "snowflake" {
// required
username = "..."
account = "..."
region = "..."
// optional, at exactly one must be set
password = "..."
oauth_access_token = "..."
private_key_path = "..."
private_key = "..."
oauth_refresh_token = "..."
oauth_client_id = "..."
oauth_client_secret = "..."
oauth_endpoint = "..."
oauth_redirect_url = "..."
password = "..."
oauth_access_token = "..."
private_key_path = "..."
private_key = "..."
private_key_passphrase = "..."
oauth_refresh_token = "..."
oauth_client_id = "..."
oauth_client_secret = "..."
oauth_endpoint = "..."
oauth_redirect_url = "..."
// optional
role = "..."
Expand Down Expand Up @@ -57,6 +58,7 @@ provider snowflake {
- **oauth_refresh_token** (String, Sensitive)
- **password** (String, Sensitive)
- **private_key** (String, Sensitive)
- **private_key_passphrase** (String, Sensitive) Supports the encryption ciphers aes-128-cbc, aes-128-gcm, aes-192-cbc, aes-192-gcm, aes-256-cbc, aes-256-gcm, and des-ede3-cbc
- **private_key_path** (String, Sensitive)
- **region** (String)
- **role** (String)
Expand Down Expand Up @@ -91,6 +93,29 @@ export SNOWFLAKE_USER="..."
export SNOWFLAKE_PRIVATE_KEY_PATH="~/.ssh/snowflake_key"
```

### Keypair Authentication Passhrase

If your private key requires a passphrase, then this can be supplied via the
environment variable `SNOWFLAKE_PRIVATE_KEY_PASSPHRASE`.

Only the ciphers aes-128-cbc, aes-128-gcm, aes-192-cbc, aes-192-gcm,
aes-256-cbc, aes-256-gcm, and des-ede3-cbc are supported on the private key

```shell
cd ~/.ssh
openssl genrsa -out snowflake_key 4096
openssl rsa -in snowflake_key -pubout -out snowflake_key.pub
openssl pkcs8 -topk8 -inform pem -in snowflake_key -outform PEM -v2 aes-256-cbc -out snowflake_key.p8
```

To export the variables into your provider:

```shell
export SNOWFLAKE_USER="..."
export SNOWFLAKE_PRIVATE_KEY_PATH="~/.ssh/snowflake_key.p8"
export SNOWFLAKE_PRIVATE_KEY_PASSPHRASE="..."
```

### OAuth Access Token

If you have an OAuth access token, export these credentials as environment variables:
Expand Down Expand Up @@ -139,24 +164,23 @@ In addition to [generic `provider` arguments](https://www.terraform.io/docs/conf
* `password` - (optional) Password for username+password auth. Cannot be used with `browser_auth` or
`private_key_path`. Can be source from `SNOWFLAKE_PASSWORD` environment variable.
* `oauth_access_token` - (optional) Token for use with OAuth. Generating the token is left to other
tools. Cannot be used with `browser_auth`, `private_key_path`, `oauth_refresh_token` or `password`.
tools. Cannot be used with `browser_auth`, `private_key_path`, `oauth_refresh_token` or `password`.
Can be sourced from `SNOWFLAKE_OAUTH_ACCESS_TOKEN` environment variable.
* `oauth_refresh_token` - (optional) Token for use with OAuth. Setup and generation of the token is
left to other tools. Should be used in conjunction with `oauth_client_id`, `oauth_client_secret`,
`oauth_endpoint`, `oauth_redirect_url`. Cannot be used with `browser_auth`, `private_key_path`,
`oauth_access_token` or `password`. Can be sourced from `SNOWFLAKE_OAUTH_REFRESH_TOKEN` environment
* `oauth_refresh_token` - (optional) Token for use with OAuth. Setup and generation of the token is
left to other tools. Should be used in conjunction with `oauth_client_id`, `oauth_client_secret`,
`oauth_endpoint`, `oauth_redirect_url`. Cannot be used with `browser_auth`, `private_key_path`,
`oauth_access_token` or `password`. Can be sourced from `SNOWFLAKE_OAUTH_REFRESH_TOKEN` environment
variable.
* `oauth_client_id` - (optional) Required when `oauth_refresh_token` is used. Can be sourced from
* `oauth_client_id` - (optional) Required when `oauth_refresh_token` is used. Can be sourced from
`SNOWFLAKE_OAUTH_CLIENT_ID` environment variable.
* `oauth_client_secret` - (optional) Required when `oauth_refresh_token` is used. Can be sourced from
* `oauth_client_secret` - (optional) Required when `oauth_refresh_token` is used. Can be sourced from
`SNOWFLAKE_OAUTH_CLIENT_SECRET` environment variable.
* `oauth_endpoint` - (optional) Required when `oauth_refresh_token` is used. Can be sourced from
* `oauth_endpoint` - (optional) Required when `oauth_refresh_token` is used. Can be sourced from
`SNOWFLAKE_OAUTH_ENDPOINT` environment variable.
* `oauth_redirect_url` - (optional) Required when `oauth_refresh_token` is used. Can be sourced from
`SNOWFLAKE_OAUTH_REDIRECT_URL` environment variable.
* `oauth_redirect_url` - (optional) Required when `oauth_refresh_token` is used. Can be sourced from
`SNOWFLAKE_OAUTH_REDIRECT_URL` environment variable.
* `private_key_path` - (optional) Path to a private key for using keypair authentication.. Cannot be
used with `browser_auth`, `oauth_access_token` or `password`. Can be source from
`SNOWFLAKE_PRIVATE_KEY_PATH` environment variable.
* `role` - (optional) Snowflake role to use for operations. If left unset, default role for user
will be used. Can come from the `SNOWFLAKE_ROLE` environment variable.

21 changes: 11 additions & 10 deletions examples/provider/provider.tf
Original file line number Diff line number Diff line change
@@ -1,19 +1,20 @@
provider snowflake {
provider "snowflake" {
// required
username = "..."
account = "..."
region = "..."

// optional, at exactly one must be set
password = "..."
oauth_access_token = "..."
private_key_path = "..."
private_key = "..."
oauth_refresh_token = "..."
oauth_client_id = "..."
oauth_client_secret = "..."
oauth_endpoint = "..."
oauth_redirect_url = "..."
password = "..."
oauth_access_token = "..."
private_key_path = "..."
private_key = "..."
private_key_passphrase = "..."
oauth_refresh_token = "..."
oauth_client_id = "..."
oauth_client_secret = "..."
oauth_endpoint = "..."
oauth_redirect_url = "..."

// optional
role = "..."
Expand Down
1 change: 1 addition & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ require (
github.com/posener/complete v1.2.1 // indirect
github.com/snowflakedb/gosnowflake v1.6.0
github.com/stretchr/testify v1.7.0
github.com/youmark/pkcs8 v0.0.0-20201027041543-1326539a0a0a
github.com/zclconf/go-cty v1.9.0 // indirect
golang.org/x/crypto v0.0.0-20210711020723-a769d52b0f97
golang.org/x/tools v0.1.5
Expand Down
2 changes: 2 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -861,6 +861,8 @@ github.com/xeipuuv/gojsonschema v1.2.0/go.mod h1:anYRn/JVcOK2ZgGU+IjEV4nwlhoK5sQ
github.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2/go.mod h1:UETIi67q53MR2AWcXfiuqkDkRtnGDLqkBTpCHuJHxtU=
github.com/xordataexchange/crypt v0.0.3-0.20170626215501-b2862e3d0a77/go.mod h1:aYKd//L2LvnjZzWKhF00oedf4jCCReLcmhLdhm1A27Q=
github.com/yalp/jsonpath v0.0.0-20180802001716-5cc68e5049a0/go.mod h1:/LWChgwKmvncFJFHJ7Gvn9wZArjbV5/FppcK2fKk/tI=
github.com/youmark/pkcs8 v0.0.0-20201027041543-1326539a0a0a h1:fZHgsYlfvtyqToslyjUt3VOPF4J7aK/3MPcK7xp3PDk=
github.com/youmark/pkcs8 v0.0.0-20201027041543-1326539a0a0a/go.mod h1:ul22v+Nro/R083muKhosV54bj5niojjWZvU8xrevuH4=
github.com/yudai/gojsondiff v1.0.0/go.mod h1:AY32+k2cwILAkW1fbgxQ5mUmMiZFgLIV+FBNExI05xg=
github.com/yudai/golcs v0.0.0-20170316035057-ecda9a501e82/go.mod h1:lgjkn3NuSvDfVJdfcVVdX+jpBxNmX4rDAzaS45IcYoM=
github.com/yudai/pp v2.0.1+incompatible/go.mod h1:PuxR/8QJ7cyCkFp/aUDS+JY727OFEZkTdatxwunjIkc=
Expand Down
56 changes: 44 additions & 12 deletions pkg/provider/provider.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package provider
import (
"crypto/rsa"
"encoding/json"
"encoding/pem"
"io"
"io/ioutil"

Expand All @@ -13,6 +14,7 @@ import (
homedir "github.com/mitchellh/go-homedir"
"github.com/pkg/errors"
"github.com/snowflakedb/gosnowflake"
"github.com/youmark/pkcs8"
"golang.org/x/crypto/ssh"

"fmt"
Expand Down Expand Up @@ -41,61 +43,61 @@ func Provider() *schema.Provider {
Optional: true,
DefaultFunc: schema.EnvDefaultFunc("SNOWFLAKE_PASSWORD", nil),
Sensitive: true,
ConflictsWith: []string{"browser_auth", "private_key_path", "private_key", "oauth_access_token", "oauth_refresh_token"},
ConflictsWith: []string{"browser_auth", "private_key_path", "private_key", "private_key_passphrase", "oauth_access_token", "oauth_refresh_token"},
},
"oauth_access_token": {
Type: schema.TypeString,
Optional: true,
DefaultFunc: schema.EnvDefaultFunc("SNOWFLAKE_OAUTH_ACCESS_TOKEN", nil),
Sensitive: true,
ConflictsWith: []string{"browser_auth", "private_key_path", "private_key", "password", "oauth_refresh_token"},
ConflictsWith: []string{"browser_auth", "private_key_path", "private_key", "private_key_passphrase", "password", "oauth_refresh_token"},
},
"oauth_refresh_token": {
Type: schema.TypeString,
Optional: true,
DefaultFunc: schema.EnvDefaultFunc("SNOWFLAKE_OAUTH_REFRESH_TOKEN", nil),
Sensitive: true,
ConflictsWith: []string{"browser_auth", "private_key_path", "private_key", "password", "oauth_access_token"},
ConflictsWith: []string{"browser_auth", "private_key_path", "private_key", "private_key_passphrase", "password", "oauth_access_token"},
RequiredWith: []string{"oauth_client_id", "oauth_client_secret", "oauth_endpoint", "oauth_redirect_url"},
},
"oauth_client_id": {
Type: schema.TypeString,
Optional: true,
DefaultFunc: schema.EnvDefaultFunc("SNOWFLAKE_OAUTH_CLIENT_ID", nil),
Sensitive: true,
ConflictsWith: []string{"browser_auth", "private_key_path", "private_key", "password", "oauth_access_token"},
ConflictsWith: []string{"browser_auth", "private_key_path", "private_key", "private_key_passphrase", "password", "oauth_access_token"},
RequiredWith: []string{"oauth_refresh_token", "oauth_client_secret", "oauth_endpoint", "oauth_redirect_url"},
},
"oauth_client_secret": {
Type: schema.TypeString,
Optional: true,
DefaultFunc: schema.EnvDefaultFunc("SNOWFLAKE_OAUTH_CLIENT_SECRET", nil),
Sensitive: true,
ConflictsWith: []string{"browser_auth", "private_key_path", "private_key", "password", "oauth_access_token"},
ConflictsWith: []string{"browser_auth", "private_key_path", "private_key", "private_key_passphrase", "password", "oauth_access_token"},
RequiredWith: []string{"oauth_client_id", "oauth_refresh_token", "oauth_endpoint", "oauth_redirect_url"},
},
"oauth_endpoint": {
Type: schema.TypeString,
Optional: true,
DefaultFunc: schema.EnvDefaultFunc("SNOWFLAKE_OAUTH_ENDPOINT", nil),
Sensitive: true,
ConflictsWith: []string{"browser_auth", "private_key_path", "private_key", "password", "oauth_access_token"},
ConflictsWith: []string{"browser_auth", "private_key_path", "private_key", "private_key_passphrase", "password", "oauth_access_token"},
RequiredWith: []string{"oauth_client_id", "oauth_client_secret", "oauth_refresh_token", "oauth_redirect_url"},
},
"oauth_redirect_url": {
Type: schema.TypeString,
Optional: true,
DefaultFunc: schema.EnvDefaultFunc("SNOWFLAKE_OAUTH_REDIRECT_URL", nil),
Sensitive: true,
ConflictsWith: []string{"browser_auth", "private_key_path", "private_key", "password", "oauth_access_token"},
ConflictsWith: []string{"browser_auth", "private_key_path", "private_key", "private_key_passphrase", "password", "oauth_access_token"},
RequiredWith: []string{"oauth_client_id", "oauth_client_secret", "oauth_endpoint", "oauth_refresh_token"},
},
"browser_auth": {
Type: schema.TypeBool,
Optional: true,
DefaultFunc: schema.EnvDefaultFunc("SNOWFLAKE_USE_BROWSER_AUTH", nil),
Sensitive: false,
ConflictsWith: []string{"password", "private_key_path", "private_key", "oauth_access_token", "oauth_refresh_token"},
ConflictsWith: []string{"password", "private_key_path", "private_key", "private_key_passphrase", "oauth_access_token", "oauth_refresh_token"},
},
"private_key_path": {
Type: schema.TypeString,
Expand All @@ -104,13 +106,21 @@ func Provider() *schema.Provider {
Sensitive: true,
ConflictsWith: []string{"browser_auth", "password", "oauth_access_token", "private_key"},
},
"private_key": &schema.Schema{
"private_key": {
Type: schema.TypeString,
Optional: true,
DefaultFunc: schema.EnvDefaultFunc("SNOWFLAKE_PRIVATE_KEY", nil),
Sensitive: true,
ConflictsWith: []string{"browser_auth", "password", "oauth_access_token", "private_key_path", "oauth_refresh_token"},
},
"private_key_passphrase": {
Type: schema.TypeString,
Description: "Supports the encryption ciphers aes-128-cbc, aes-128-gcm, aes-192-cbc, aes-192-gcm, aes-256-cbc, aes-256-gcm, and des-ede3-cbc",
Optional: true,
DefaultFunc: schema.EnvDefaultFunc("SNOWFLAKE_PRIVATE_KEY_PASSPHRASE", nil),
Sensitive: true,
ConflictsWith: []string{"browser_auth", "password", "oauth_access_token", "oauth_refresh_token"},
},
"role": {
Type: schema.TypeString,
Optional: true,
Expand Down Expand Up @@ -234,6 +244,7 @@ func ConfigureProvider(s *schema.ResourceData) (interface{}, error) {
browserAuth := s.Get("browser_auth").(bool)
privateKeyPath := s.Get("private_key_path").(string)
privateKey := s.Get("private_key").(string)
privateKeyPassphrase := s.Get("private_key_passphrase").(string)
oauthAccessToken := s.Get("oauth_access_token").(string)
region := s.Get("region").(string)
role := s.Get("role").(string)
Expand All @@ -258,6 +269,7 @@ func ConfigureProvider(s *schema.ResourceData) (interface{}, error) {
browserAuth,
privateKeyPath,
privateKey,
privateKeyPassphrase,
oauthAccessToken,
region,
role,
Expand All @@ -281,6 +293,7 @@ func DSN(
browserAuth bool,
privateKeyPath,
privateKey,
privateKeyPassphrase,
oauthAccessToken,
region,
role string) (string, error) {
Expand All @@ -303,14 +316,14 @@ func DSN(
if err != nil {
return "", errors.Wrap(err, "Private Key file could not be read")
}
rsaPrivateKey, err := ParsePrivateKey(privateKeyBytes)
rsaPrivateKey, err := ParsePrivateKey(privateKeyBytes, []byte(privateKeyPassphrase))
if err != nil {
return "", errors.Wrap(err, "Private Key could not be parsed")
}
config.PrivateKey = rsaPrivateKey
config.Authenticator = gosnowflake.AuthTypeJwt
} else if privateKey != "" {
rsaPrivateKey, err := ParsePrivateKey([]byte(privateKey))
rsaPrivateKey, err := ParsePrivateKey([]byte(privateKey), []byte(privateKeyPassphrase))
if err != nil {
return "", errors.Wrap(err, "Private Key could not be parsed")
}
Expand Down Expand Up @@ -348,7 +361,26 @@ func ReadPrivateKeyFile(privateKeyPath string) ([]byte, error) {
return privateKeyBytes, nil
}

func ParsePrivateKey(privateKeyBytes []byte) (*rsa.PrivateKey, error) {
func ParsePrivateKey(privateKeyBytes []byte, passhrase []byte) (*rsa.PrivateKey, error) {
privateKeyBlock, _ := pem.Decode(privateKeyBytes)
if privateKeyBlock == nil {
return nil, fmt.Errorf("Could not parse private key, key is not in PEM format")
}

if privateKeyBlock.Type == "ENCRYPTED PRIVATE KEY" {
if len(passhrase) == 0 {
return nil, fmt.Errorf("Private key requires a passphrase, but private_key_passphrase was not supplied")
}
privateKey, err := pkcs8.ParsePKCS8PrivateKeyRSA(privateKeyBlock.Bytes, passhrase)
if err != nil {
return nil, errors.Wrap(
err,
"Could not parse encrypted private key with passphrase, only ciphers aes-128-cbc, aes-128-gcm, aes-192-cbc, aes-192-gcm, aes-256-cbc, aes-256-gcm, and des-ede3-cbc are supported",
)
}
return privateKey, nil
}

privateKey, err := ssh.ParseRawPrivateKey(privateKeyBytes)
if err != nil {
return nil, errors.Wrap(err, "Could not parse private key")
Expand Down
4 changes: 2 additions & 2 deletions pkg/provider/provider_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ func TestDSN(t *testing.T) {
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got, err := provider.DSN(tt.args.account, tt.args.user, tt.args.password, tt.args.browserAuth, "", "", "", tt.args.region, tt.args.role)
got, err := provider.DSN(tt.args.account, tt.args.user, tt.args.password, tt.args.browserAuth, "", "", "", "", tt.args.region, tt.args.role)
if (err != nil) != tt.wantErr {
t.Errorf("DSN() error = %v, wantErr %v", err, tt.wantErr)
return
Expand Down Expand Up @@ -84,7 +84,7 @@ func TestOAuthDSN(t *testing.T) {
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got, err := provider.DSN(tt.args.account, tt.args.user, "", false, "", "", tt.args.oauthAccessToken, tt.args.region, tt.args.role)
got, err := provider.DSN(tt.args.account, tt.args.user, "", false, "", "", "", tt.args.oauthAccessToken, tt.args.region, tt.args.role)

if (err != nil) != tt.wantErr {
t.Errorf("DSN() error = %v, dsn = %v, wantErr %v", err, got, tt.wantErr)
Expand Down

0 comments on commit a1c4067

Please sign in to comment.