diff --git a/README.md b/README.md index 90aeff3..8dffd70 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # Delinea Secret Server - Terraform Provider -The [Delinea](https://delinea.com/) [Secret Server](https://delinea.com/products/secret-server/) [Terraform](https://www.terraform.io/) Provider allows you to access and reference Secrets in your vault for use in Terraform configurations. +The [Delinea](https://delinea.com/) [Secret Server](https://delinea.com/products/secret-server/) [Terraform](https://www.terraform.io/) Provider allows you to access and reference Secrets in your vault for use in Terraform configurations. Detail documentation of this integration is available [here](https://docs.delinea.com/online-help/integrations/terraform/configure.htm) ## Install via Registry @@ -23,7 +23,7 @@ Terraform 0.13 uses a different file system layout for 3rd party providers. More └───terraform.delinea.com DelineaXPM └───tss - └───2.0.7 + └───2.0.8 └───windows_amd64 ``` @@ -34,7 +34,7 @@ Terraform 0.13 uses a different file system layout for 3rd party providers. More └───terraform.delinea.com DelineaXPM └───tss - └───2.0.7 + └───2.0.8 ├───linux_amd64 ``` @@ -156,3 +156,28 @@ Alternatively, the domain can be provided with an environment variable: ```sh $ export TSS_DOMAIN="mycompany.com" ``` + +## Encrypt terraform state file using script wrapper + +Terraform supports multiple backends to securely store state files, such as AWS S3, Azure Blob Storage, and others. These backends also include built-in state locking mechanisms. However, when storing state files on a local machine drive, you need to manually encrypt the state file data to keep it secure. + +To encrypt or decrypt state file data during the Terraform workflow, you must perform encryption before executing Terraform commands and decryption afterward. This can be achieved by creating script wrappers around Terraform commands like terraform init, terraform apply, and terraform destroy. + +To use these script wrappers, place the script files in the Terraform executable directory and set the required user credentials in environment variables. For instructions on setting environment variables, refer to the section titled "Environment Variables" above. + +Scripts for reference and more detailed information are available [here](https://docs.delinea.com/online-help/integrations/terraform/index.htm). You can modify file paths in these scripts as needed. + +You can then execute the script wrappers as shown below: + +Usage (For Linux) +``` +$ sh terraform_init.sh +$ sh terraform_apply.sh +$ sh terraform_destroy.sh +``` +Usage (For Windows) +``` +> terraform_init.bat +> terraform_apply.bat +> terraform_destroy.bat +``` \ No newline at end of file diff --git a/delinea/encryption_helper.go b/delinea/encryption_helper.go new file mode 100644 index 0000000..e3131a6 --- /dev/null +++ b/delinea/encryption_helper.go @@ -0,0 +1,136 @@ +package delinea + +import ( + "crypto/aes" + "crypto/cipher" + "crypto/rand" + "crypto/sha256" + "encoding/base64" + "fmt" + "io" + "log" + "os" + + "golang.org/x/crypto/pbkdf2" +) + +// Define constants for salt length and key length +const saltLength = 16 +const keyLength = 32 +const iterations = 100000 + +func fileExists(filename string) bool { + _, err := os.Stat(filename) + if os.IsNotExist(err) { + return false + } + return err == nil +} + +// EncryptFile encrypts the file content +func EncryptFile(passphrase, stateFile string) error { + if !fileExists(stateFile) { + return nil + } + + // Read the input file + data, err := os.ReadFile(stateFile) + if err != nil { + return fmt.Errorf("failed to read input file: %v", err) + } + + // Generate a random salt + salt := make([]byte, saltLength) + if _, err := rand.Read(salt); err != nil { + return fmt.Errorf("failed to generate salt: %v", err) + } + + // Derive the encryption key using PBKDF2 + key := pbkdf2.Key([]byte(passphrase), salt, iterations, keyLength, sha256.New) + + // Encrypt the data + block, err := aes.NewCipher(key) + if err != nil { + return fmt.Errorf("failed to create cipher block: %v", err) + } + + gcm, err := cipher.NewGCM(block) + if err != nil { + return fmt.Errorf("failed to create GCM: %v", err) + } + + nonce := make([]byte, gcm.NonceSize()) + if _, err := io.ReadFull(rand.Reader, nonce); err != nil { + return fmt.Errorf("failed to generate nonce: %v", err) + } + + // Encrypt the data using GCM + encryptedData := gcm.Seal(nonce, nonce, data, nil) + + // Prepend the salt to the encrypted data + finalData := append(salt, encryptedData...) + + // Write the encrypted data to the state file + err = os.WriteFile(stateFile, []byte(base64.StdEncoding.EncodeToString(finalData)), 0644) + if err != nil { + return fmt.Errorf("failed to write encrypted data to state file: %v", err) + } + + log.Printf("[DEBUG] File encrypted successfully: %s\n", stateFile) + return nil +} + +// DecryptFile decrypts the content of the state file +func DecryptFile(passphrase, stateFile string) error { + if !fileExists(stateFile) { + return nil + } + + // Read the encrypted file + encryptedBase64Data, err := os.ReadFile(stateFile) + if err != nil { + return fmt.Errorf("failed to read encrypted file: %v", err) + } + + // Decode the base64-encoded encrypted data + encryptedData, err := base64.StdEncoding.DecodeString(string(encryptedBase64Data)) + if err != nil { + return fmt.Errorf("failed to decode base64 data: %v", err) + } + + // Extract the salt and encrypted data + salt := encryptedData[:saltLength] + encryptedContent := encryptedData[saltLength:] + + // Derive the decryption key using PBKDF2 + key := pbkdf2.Key([]byte(passphrase), salt, iterations, keyLength, sha256.New) + + // Decrypt the data + block, err := aes.NewCipher(key) + if err != nil { + return fmt.Errorf("failed to create cipher block: %v", err) + } + + gcm, err := cipher.NewGCM(block) + if err != nil { + return fmt.Errorf("failed to create GCM: %v", err) + } + + nonceSize := gcm.NonceSize() + nonce, ciphertext := encryptedContent[:nonceSize], encryptedContent[nonceSize:] + + // Decrypt the data using GCM + decryptedData, err := gcm.Open(nil, nonce, ciphertext, nil) + if err != nil { + return fmt.Errorf("failed to decrypt data: %v", err) + } + + // Write the decrypted data to the state file + err = os.WriteFile(stateFile, decryptedData, 0644) + if err != nil { + return fmt.Errorf("failed to write decrypted data to state file: %v", err) + } + + log.Printf("[DEBUG] File decrypted successfully: %s\n", stateFile) + return nil +} diff --git a/examples/secrets/secret_create.tf b/examples/secrets/secret_create.tf index 7cbdcf8..d6a0b2f 100644 --- a/examples/secrets/secret_create.tf +++ b/examples/secrets/secret_create.tf @@ -3,7 +3,7 @@ terraform { required_providers { tss = { source = "DelineaXPM/tss" - version = "2.0.7" + version = "2.0.8" } } } @@ -49,7 +49,6 @@ provider "tss" { server_url = var.tss_server_url } - resource "tss_resource_secret" "secret_name" { name = var.tss_secret_name folderid = var.tss_secret_folderid diff --git a/examples/secrets/secret_get.tf b/examples/secrets/secret_get.tf index ee7e1e1..b8f52fb 100644 --- a/examples/secrets/secret_get.tf +++ b/examples/secrets/secret_get.tf @@ -3,7 +3,7 @@ terraform { required_providers { tss = { source = "DelineaXPM/tss" - version = "2.0.7" + version = "2.0.8" } } } diff --git a/examples/secrets/secrets_get.tf b/examples/secrets/secrets_get.tf index 8c4aa7e..998f7bc 100644 --- a/examples/secrets/secrets_get.tf +++ b/examples/secrets/secrets_get.tf @@ -3,7 +3,7 @@ terraform { required_providers { tss = { source = "DelineaXPM/tss" - version = "2.0.7" + version = "2.0.8" } } } diff --git a/go.mod b/go.mod index 2dc5f54..fb703af 100644 --- a/go.mod +++ b/go.mod @@ -3,6 +3,7 @@ module github.com/DelineaXPM/terraform-provider-tss/v2 require ( github.com/DelineaXPM/tss-sdk-go/v2 v2.0.1 github.com/hashicorp/terraform-plugin-sdk/v2 v2.34.0 + golang.org/x/crypto v0.27.0 ) require ( @@ -36,11 +37,12 @@ require ( github.com/vmihailenco/msgpack/v5 v5.4.1 // indirect github.com/vmihailenco/tagparser/v2 v2.0.0 // indirect github.com/zclconf/go-cty v1.14.4 // indirect - golang.org/x/mod v0.16.0 // indirect - golang.org/x/net v0.23.0 // indirect - golang.org/x/sys v0.20.0 // indirect - golang.org/x/text v0.15.0 // indirect - golang.org/x/tools v0.13.0 // indirect + golang.org/x/mod v0.17.0 // indirect + golang.org/x/net v0.25.0 // indirect + golang.org/x/sync v0.8.0 // indirect + golang.org/x/sys v0.25.0 // indirect + golang.org/x/text v0.18.0 // indirect + golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d // indirect google.golang.org/appengine v1.6.8 // indirect google.golang.org/genproto/googleapis/rpc v0.0.0-20240227224415-6ceb2ff114de // indirect google.golang.org/grpc v1.63.2 // indirect diff --git a/go.sum b/go.sum index 6ee7a59..891d512 100644 --- a/go.sum +++ b/go.sum @@ -102,20 +102,22 @@ github.com/zclconf/go-cty-debug v0.0.0-20191215020915-b22d67c1ba0b h1:FosyBZYxY3 github.com/zclconf/go-cty-debug v0.0.0-20191215020915-b22d67c1ba0b/go.mod h1:ZRKQfBXbGkpdV6QMzT3rU1kSTAnfu1dO8dPKjYprgj8= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= +golang.org/x/crypto v0.27.0 h1:GXm2NjJrPaiv/h1tb2UH8QfgC/hOf/+z0p6PT8o1w7A= +golang.org/x/crypto v0.27.0/go.mod h1:1Xngt8kV6Dvbssa53Ziq6Eqn0HqbZi5Z6R0ZpwQzt70= golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= -golang.org/x/mod v0.16.0 h1:QX4fJ0Rr5cPQCF7O9lh9Se4pmwfwskqZfq5moyldzic= -golang.org/x/mod v0.16.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= +golang.org/x/mod v0.17.0 h1:zY54UmvipHiNd+pm+m0x9KhZ9hl1/7QNMyxXbc6ICqA= +golang.org/x/mod v0.17.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= -golang.org/x/net v0.23.0 h1:7EYJ93RZ9vYSZAIb2x3lnuvqO5zneoD6IvWjuhfxjTs= -golang.org/x/net v0.23.0/go.mod h1:JKghWKKOSdJwpW2GEx0Ja7fmaKnMsbu+MWVZTokSYmg= +golang.org/x/net v0.25.0 h1:d/OCCoBEUq33pjydKrGQhw7IlUPI2Oylr+8qLx49kac= +golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM= golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.6.0 h1:5BMeUDZ7vkXGfEr1x9B4bRcTH4lpkTkpdh0T/J+qjbQ= -golang.org/x/sync v0.6.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= +golang.org/x/sync v0.8.0 h1:3NFvSEYkUoMifnESzZl15y791HH1qU2xm6eCJU5ZPXQ= +golang.org/x/sync v0.8.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= @@ -128,8 +130,8 @@ golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.20.0 h1:Od9JTbYCk261bKm4M/mw7AklTlFYIa0bIp9BgSm1S8Y= -golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.25.0 h1:r+8e+loiHxRqhXVl6ML1nO3l1+oFoWbnlu2Ehimmi34= +golang.org/x/sys v0.25.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= @@ -137,13 +139,13 @@ golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= golang.org/x/text v0.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ= -golang.org/x/text v0.15.0 h1:h1V/4gjBv8v9cjcR6+AR5+/cIYK5N/WAgiv4xlsEtAk= -golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= +golang.org/x/text v0.18.0 h1:XvMDiNzPAl0jr17s6W9lcaIhGUfUORdGCNsuLmPG224= +golang.org/x/text v0.18.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= -golang.org/x/tools v0.13.0 h1:Iey4qkscZuv0VvIt8E0neZjtPVQFSc870HQ448QgEmQ= -golang.org/x/tools v0.13.0/go.mod h1:HvlwmtVNQAhOuCjW7xxvovg8wbNq7LwfXh/k7wXUl58= +golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d h1:vU5i/LfpvrRCpgM/VPfJLg5KjxD3E+hfT1SH+d9zLwg= +golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= diff --git a/main.go b/main.go index ff0dd7b..7f93c7f 100644 --- a/main.go +++ b/main.go @@ -1,12 +1,43 @@ package main import ( + "log" + "os" + "github.com/DelineaXPM/terraform-provider-tss/v2/delinea" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" "github.com/hashicorp/terraform-plugin-sdk/v2/plugin" ) func main() { + + if len(os.Args) >= 2 { + action := os.Args[1] + stateFile := os.Args[2] + + passphrase := os.Getenv("TFSTATE_PASSPHRASE") + if passphrase == "" { + log.Println("Passphrase not set in TFSTATE_PASSPHRASE environment variable") + return + } + + switch action { + case "encrypt": + err := delinea.EncryptFile(passphrase, stateFile) + if err != nil { + log.Printf("[DEBUG] Error encrypting file: %v\n", err) + } + case "decrypt": + err := delinea.DecryptFile(passphrase, stateFile) + if err != nil { + log.Printf("[DEBUG] Error decrypting file: %v\n", err) + } + default: + log.Println("[DEBUG] Invalid action. Use 'encrypt' or 'decrypt'.") + } + return + } + plugin.Serve(&plugin.ServeOpts{ ProviderFunc: func() *schema.Provider { return delinea.Provider()