diff --git a/go.mod b/go.mod index 630afe274..9a8d523e7 100644 --- a/go.mod +++ b/go.mod @@ -14,7 +14,7 @@ require ( github.com/fatih/color v1.19.0 github.com/google/go-cmp v0.7.0 github.com/kong/go-apiops v0.4.4 - github.com/kong/go-database-reconciler v1.40.3 + github.com/kong/go-database-reconciler v1.40.4-0.20260708084815-0ffa259e99e2 github.com/kong/go-kong v0.76.1 github.com/mitchellh/go-homedir v1.1.0 github.com/spf13/cobra v1.10.2 diff --git a/go.sum b/go.sum index 831df2f38..237e71685 100644 --- a/go.sum +++ b/go.sum @@ -256,6 +256,8 @@ github.com/kong/go-apiops v0.4.4 h1:16raOAuErHV32OuLwQEO46CBZ86OGMmTdEHatZ5nJB0= github.com/kong/go-apiops v0.4.4/go.mod h1:Xt99d90LallLVwYJAGaufiNbBdsK0KKboe7gR4Ryths= github.com/kong/go-database-reconciler v1.40.3 h1:myK/p/BjWgKwhLbbuHY3//Q9Ui2YPtR0mkXc7A96XL0= github.com/kong/go-database-reconciler v1.40.3/go.mod h1:YlfE6omuZS9Xbm+IlfprZExA1BFESejy4CJhwvWBvKo= +github.com/kong/go-database-reconciler v1.40.4-0.20260708084815-0ffa259e99e2 h1:Ih+XwfIvcB5rpY/34gAvZnry1IbPhMZ59SnrHC6x22A= +github.com/kong/go-database-reconciler v1.40.4-0.20260708084815-0ffa259e99e2/go.mod h1:YlfE6omuZS9Xbm+IlfprZExA1BFESejy4CJhwvWBvKo= github.com/kong/go-kong v0.76.1 h1:lImAHvog7ehm4XL2rs8ZyCx98k56ihNazLG55bvHgVM= github.com/kong/go-kong v0.76.1/go.mod h1:Wx5aTcMjyUnIF94M5NYFWb/EnuEkqB5STrWvybFSYYQ= github.com/kong/go-slugify v1.0.0 h1:vCFAyf2sdoSlBtLcrmDWUFn0ohlpKiKvQfXZkO5vSKY= diff --git a/tests/integration/sync_test.go b/tests/integration/sync_test.go index 29daf245c..06640edef 100644 --- a/tests/integration/sync_test.go +++ b/tests/integration/sync_test.go @@ -5019,7 +5019,8 @@ func Test_Sync_Unsupported_Formats(t *testing.T) { kongFile: "testdata/sync/001-create-a-service/kong.yaml", expectedError: errors.New( "cannot apply '1.1' config format version to Kong version 3.0 or above.\n" + - utils.UpgradeMessage), + utils.UpgradeMessage, + ), }, } for _, tc := range tests { @@ -12985,3 +12986,64 @@ func testSyncCustomPluginDefinitionsKonnectImpl(t *testing.T) { }) } } + +const ( + certAMask = `"-----BEGIN CERTIFICATE-----\nMIIErDCCApSgAwIBAgIUaZRSadvXi4QaZssfTWp+gNNzU0kwDQYJKoZIhvcNAQEL\nBQAwSTEUMBIGA1UEAwwLZXhhbXBsZS5jb20xCzAJBgNVBAYTAkdCMRAwDgYDVQQI\nDAdFbmdsYW5kMRIwEAYDVQQKDAlBbGljZSBMdGQwHhcNMjYwMjA1MDgxMzAxWhcN\nMjgwNTEwMDgxMzAxWjBUMQswCQYDVQQGEwJNWTETMBEGA1UECAwKU29tZS1TdGF0\nZTESMBAGA1UECgwJbXljb21wYW55MRwwGgYDVQQDDBNrb25nLWNwLmV4YW1wbGUu\nY29tMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAzFtAvLv3CgVY8bYk\nU3XdwT+XHn55LCpQAWdoLyKwfnxCU0EXQ6aWjcVTeHQAxRwbGjD3pDnkaKYk2sFg\nIOqJxJTgk+lcEjVoaMt1ZlEvWv8uLkmV9qkR3GkgkjEsdZpS0O0j9wktjFICfHT8\nxhR2aNRGs0l8msRX6AWpwfCIFv4NODU+q8gTY2bN7jAb4GMlCid7Eryz+wpeT8OT\nvuBTfMZDYXXYR+YnOsBhhJ0R8vEpjO7nhih600ABFJE1OsftNoffYQuSvX32Qpz0\n1SxbuFdATOi1gJKqrr8wzPE9xZ0wwZPeWSq0bHlcNwRJxfS3jPhA87NrlpBsVqar\nek6uZQIDAQABo4GAMH4wHwYDVR0jBBgwFoAUTYO0qty6yVVauLKCgBsnJH8IH/ow\nCQYDVR0TBAIwADALBgNVHQ8EBAMCBPAwJAYDVR0RBB0wG4ITa29uZy1jcC5leGFt\ncGxlLmNvbYcEwKgBCjAdBgNVHQ4EFgQUkd88zLNJe1VTHVYhDpc+pzeLjvowDQYJ\nKoZIhvcNAQELBQADggIBAEfLgqkuXU2MXFiiIsFAedRf/mV3s3Mw7pdivfJ6kAyL\nO5FWgUTYlfOj96QI2Sy03Ucc8wyXIqfPThB8Of0W8S2IX17sO9NIJthfeA6mqLsF\nlHadOqn5hmHkM98sYFg6x3c1Ca+lgmqV4bGwTq5/L5IEV80GsFZgz24ptz990vWy\nDrwbRNZkRQjVNAl1Mi+DEqGGEam3GBJWwmrUZAR9pGvdYmW2T8LLLyPd/TneMaF0\nK0EnPp6vpVDFx5fLRFifzWRTXsFrTkzdKIMrjo4yULzjZMeIvQC7ve6tkAqY/Yzi\ndTKSfXdzB+mA+LX91irgVBJwJjm7pvzOLbipwnXPN/6S+ZgCjtGbInbphbTK9peX\naqr4LVFy91JvjfCUWTa53QvBed02z32wQTuUp6k3TG6Azulq2vDvlZ2dHAAAN1LX\nriXH3R4G51SLiuhm0mMcHl2XB5RRm/VejwYwyVGtCUzXtu8n1iKlfTvbvf/AE9JI\n8Z5tf5mTWmZWkUTAVHUhjaHl+2/u878neMSl5jMkkydc3scgV4V9jj4DwdlG7aQd\nG5eX5Ais37lQOxOrRheqb+lBaOiQUZ1wAmmg2lhdmOAEA+4v7BxI2LE0ltrlpwUs\npFFME++aEsHprFc/sCqX/Qz3J5dw8C9jS2ys2NdtYvZbik+pD3fwao78hkEoXVHF\n-----END CERTIFICATE-----"` //nolint + keyAMask = `"-----BEGIN PRIVATE KEY-----\nMIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQDMW0C8u/cKBVjx\ntiRTdd3BP5cefnksKlABZ2gvIrB+fEJTQRdDppaNxVN4dADFHBsaMPekOeRopiTa\nwWAg6onElOCT6VwSNWhoy3VmUS9a/y4uSZX2qRHcaSCSMSx1mlLQ7SP3CS2MUgJ8\ndPzGFHZo1EazSXyaxFfoBanB8IgW/g04NT6ryBNjZs3uMBvgYyUKJ3sSvLP7Cl5P\nw5O+4FN8xkNhddhH5ic6wGGEnRHy8SmM7ueGKHrTQAEUkTU6x+02h99hC5K9ffZC\nnPTVLFu4V0BM6LWAkqquvzDM8T3FnTDBk95ZKrRseVw3BEnF9LeM+EDzs2uWkGxW\npqt6Tq5lAgMBAAECggEAF5IC4oHvmXlKa/6tK+8xV6PFA2BlC4IJ3k+WBbHDl6Mv\nZzjfvQ7O2KVE3mTK3fFC8v9o4MlIngVjxNN/CezjXQgq6wSEeRkt5MBq1xVQF/fM\n1Mvp/xw7EigR+KqxTRMysCRZ/JlTZpbi8IXxzcr4aVjqmq1EMPmqhfl5PgRHPhd0\ne1D+ovOSNBsEkCUW0wrVeThHPrTuVr6enjWmw/BsVXhTK3lXinRkYbPRo2L2VFKM\n5XO/t5lmEm6OpRoA/cis32LlQMGEUROIP8xBDcSy3zDBWcNfcc+xohszoMgKpBvr\nOrWahhIwSItH2EfE3GzTrANnWLOfPju88NLpPT7u9QKBgQD1m7fsUR8R5rEPx17w\nCOlRFR6g4+DYXgA2XM9J+ZBg7XMHkiiove5aFaJ5U0B2L2MGxTpzAHkOURIwaE1r\n5UvUrsg6Y449sswjx8D+kP0R7PG0/zHwRqkFBVW4+0z1x0ud3XSpZix05xCAhIkl\nTFJLtwmX+1iii3rafqIY1ln41wKBgQDVALgvGbrzNM2P/ErBKA6N0STg2vbqffOF\nSsMUlGbvEz+Rs/zI/y8CjMbS2YZGPYOYiQd9uONaXJC831BqaYLqKhSg+Mo1JyXf\ngeudD2UDbD5GKeRh0hJxEr5hovv7cviguK0imrY8LWd9hzZR86riehUnj6OiiqCw\naBe9Rgl/IwKBgQChp2dL7Cy1/I+nogKTnFnsmyj4nmP8JQqgvT9WIiNsGH0wi3ul\nbLU3p6uzjR8Naq0T5LZOwQ3sZWHCaLQkgtwBVTTGcYZKh6cjGbOmGcnnhXulNJ4q\n1mcivdspEIYb8puLgRnq8SvrmbXb0N/vo6c63HM9DBrPiVxH+Tc5f8PiKwKBgE8K\n9U6FPdsCvUWxWgxT0RLPid/nYbmjA7tzAvGgIJKXkf9/y07LY6aOlzmnfyyRp0Pz\nldQRyEzADPYJ6M2UWgCLnRwfNuqCnItUqxqdm96pTHK9GQMz8aQEUKHq2Zd2Es2R\nuq4cu4OZVc4/XukKg1+H7lG/+/lVeEv+VNDoLJ4BAoGAfCIBneN9wNBKrDPtVKZB\nkDYmWTkLzd1UDzXTD9LTc/3PXR6JRZURq7Z5vvjdBOh0uUxxG7RYeN7wIjWiPfww\nmOUReWlh+n1kyxOyysfZkKJA+jcVs5AhMLzvOCGQL7Yy5uAteZHxQ1Lzr1LlZPOs\nSGsNpIfCXpdZTJE3NAEShjE=\n-----END PRIVATE KEY-----"` //nolint + certBMask = `"-----BEGIN CERTIFICATE-----\nMIIFKDCCAxCgAwIBAgICEAMwDQYJKoZIhvcNAQELBQAwSTELMAkGA1UEBhMCR0Ix\nEDAOBgNVBAgMB0VuZ2xhbmQxEjAQBgNVBAoMCUFsaWNlIEx0ZDEUMBIGA1UEAwwL\nZXhhbXBsZS5jb20wHhcNMjUwNzMxMDQ1MDE3WhcNMjYwNzMxMDQ1MDE3WjAMMQow\nCAYDVQQDDAEvMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAgfMdN5wX\nelFbX9Nl+jd6ey7PEtuovm2bs2FpvTdRod0NbAlG+psr4Knp8FWmA6m3ZlkM2i5s\n6kz8KiYbi1jArhSw355LmcOy8dpW/PLTsOqi5cDRYgDH5BmbRqPN/H/5gSSeHpXW\nPvt1wL8i+nzOA7p1gulicQoMntKKUVs6lasJ5VbqEkRYWREdsvfjwshrMmEELLhK\n6nIRi1xY00SAMIaqUscH5H5NQO0oH2B4IekUI3ykgLyM1li5IlDmvdxZeAyWKawP\nol558uzlSKbNNI8G4aQ8gqhlKxW+QeE27y6N/8cndQKMJ8tTa17nvImDMGEDtTlM\ni67OTLe9qYaezwIDAQABo4IBVTCCAVEwCQYDVR0TBAIwADARBglghkgBhvhCAQEE\nBAMCBaAwMwYJYIZIAYb4QgENBCYWJE9wZW5TU0wgR2VuZXJhdGVkIENsaWVudCBD\nZXJ0aWZpY2F0ZTAdBgNVHQ4EFgQUAWacUI67lU62QGE4/E9tNINMvrAwHwYDVR0j\nBBgwFoAUi4zJs4OH+vQWVIzA0f2gqH8GFtgwDgYDVR0PAQH/BAQDAgXgMB0GA1Ud\nJQQWMBQGCCsGAQUFBwMCBggrBgEFBQcDBDCBjAYDVR0RAQH/BIGBMH+BFmpvaG4u\nZG9lQG15Y29tcGFueS5jb22kZTBjMQswCQYDVQQGEwJESzEgMB4GA1UECgwXTXkg\nRXhhbXBsZSBPcmdhbml6YXRpb24xGDAWBgNVBAsMD015IEV4YW1wbGUgVW5pdDEY\nMBYGA1UEAwwPTXkgRXhhbXBsZSBOYW1lMA0GCSqGSIb3DQEBCwUAA4ICAQBybIhg\nLbhL4VG4/J+IyksjJ71KgZZlgOHdg7d8fROjawtyZKh2ENZGYQOuZo79ppM7xhjf\np8RJE9meKH2VxNWyzvQ0CW4xqEAAq5Fyu9eejUe6dPVpbwmG3TLEiw+3L1af3zEl\npalnTWVtUbCao06BbjuQ8MEnzH4sn1N3zUHnBDAh7zCHa79GhLlAeoLWQlgfMdTz\nBNymnsSoqjtvmDwc1FsrVdcSlmbPbvmzorAgLKKIqu+xRFa9nWvaJlgvuqtodcQJ\n3UGBCSAYrs23sCEfXXs/IK/IoiFTrKHPsaBr7NJfhKII6Fm3hJtgPXKIkxCQYkoP\nNz4fCRE72fCRbLJPaObc5X7i9ayoZoOqg5YgOLuSXiBp+LBu5wghGZABle/hjaJU\n6LhKl5olsH0HHQrhzj1xPZ5dplZCIJ6t+e11zdNBRTTjCkmhZGqOzylMm0NDufzP\nAHd9zGvotoM0gwDQNd9JuQ/A+G8+Wzb8Eb7EkdImxYi7qZsi01SG4GGoFHXIfoLp\ncB2quk7bkBSqx/wtJNQnLdi04E78Cy9Yzre6HmZMjXXrspkLM3qlDA0g5JMEy5/n\nywUqL9srpjW7qrP8NGxzzQ9z40bktDYPqVy57NK3tQtU9C2IAosU6XHO+u/H+067\nUrmTdgxd0MfsO5iH6OcBXGAkakvKx1vYGfHWjg==\n-----END CERTIFICATE-----"` //nolint + keyBMask = `"-----BEGIN PRIVATE KEY-----\nMIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQCB8x03nBd6UVtf\n02X6N3p7Ls8S26i+bZuzYWm9N1Gh3Q1sCUb6myvgqenwVaYDqbdmWQzaLmzqTPwq\nJhuLWMCuFLDfnkuZw7Lx2lb88tOw6qLlwNFiAMfkGZtGo838f/mBJJ4eldY++3XA\nvyL6fM4DunWC6WJxCgye0opRWzqVqwnlVuoSRFhZER2y9+PCyGsyYQQsuErqchGL\nXFjTRIAwhqpSxwfkfk1A7SgfYHgh6RQjfKSAvIzWWLkiUOa93Fl4DJYprA+iXnny\n7OVIps00jwbhpDyCqGUrFb5B4TbvLo3/xyd1Aowny1NrXue8iYMwYQO1OUyLrs5M\nt72php7PAgMBAAECggEAMLfuijyw6KC7uw9MOiS2fJwt8gn8F3iMWRH6TIAC0MZJ\nE+nefHHUX2QUWpAhr1nfcjWMhffFyYmXOE4VIYbipeEH9q4n9bvSyK1s02ROTROK\ngsxpM27Lgg6/khozTajE58/Mn/bY3b/0g5ClEnfb3LX4/F13FJ4J4TlZteqX6igu\nyKRCGtB1Z0Af3UU9RSe1Z+XhQLZjj7khU4hZ5sMBQmEtQvtaO9BNbAV/RLNvOrQh\nNewolh+W5U7KZtjWuIMhN1FI/I0ax8Uezxd47q0y8JdE2CYzRnsn1Fl9x2DfshG3\nCBxWrYFYRYkY0occ61ATW7DE+K4XmB1dm2gjZcg1EQKBgQC2zX+5zKGOJ743kIwd\nr5d5RuZEPNAOmIR8AvBAcjwmUkm19L7VXxgz2cuyuN+WAofm/fFulMb/57YlBpQd\nRjIuMRAantq/K/QfMJ3EZ0dcD7GwbMIPgLDnMInJ5BL6S/QorSKUTsfd0a6vnYXK\nLs0TWeR6wk1bfvtTrM5AGnJGNQKBgQC1+9KoqkzplA+poECx8OGGIHVMhx1O6B/L\nNK0N8r8KkEGVnQUpwb3GQRH2U13QHeLKz0IqqoD9mJ4J7rv/53bjdqN1KtsS2uY7\nn8w4onXTkOjeBD2u7ZpnVR4X0wkoH25loyCeIf+wk+eE3Oo6w8J+bVr1FdCenFSQ\nGdx1RBthcwKBgQCj2ijtan04xWjbt0XQag601xcY/QVkv9KosZ2Vto8xrjqBzQGs\nkZNJDqRl6w4GEPit1NsuSk7hHFmIcJAceUoLW1Da+56JEqW7u0D+xbAR1cNopuN/\nL9PhNgGbpEJadfBA8Yd4NfEyY3tX0DMW2cJZ4Q5J9pWOOJCgHNm2Xb4mLQKBgDuU\n/E6um0nqaQpfZm8b76qrW9w3bJgWFbrBP1uOu4+o1WA3raYTQZ/0U/4zI0btUgCW\nxRIVCvB8lFmqKTFKxjkI8dF9u4Y71x90XTe5sRwYmLV/6/Ym4uOsEZM5TAkv6UQr\nmZVMilt0wVtVvPaKe1ZLVXlisBAOHP2lCfH234QTAoGAU+ketoVbPSTtjPNmot4T\nghFDjsc4kzc5LGahRFRw0Pc178QD7GJkTPFFea1rFUrS5SWToMvHjlgSH60PXNu2\nGWXu9hx1FA3mkywL578hNdrZVYK9yQunzoz7E3pbyAdAATfdvNPZl2ag9cpICD/Z\nbx8IrEy3OPzoCjbWVWAswx0=\n-----END PRIVATE KEY-----"` //nolint + jwkKeyMask = `{"kid":"vsR8NCNV_1_LB06LqudGa2r-T0y4Z6VQVYue9IQz6A4","kty":"RSA","alg":"A256GCM","n":"v2KAzzfruqctVHaE9WSCWIg1xAhMwxTIK-i56WNqPtpWBo9AqxcVea8NyVctEjUNq_mix5CklNy3ru7ARh7rBG_LU65fzs4fY_uYalul3QZSnr61Gj-cTUB3Gy4PhA63yXCbYRR3gDy6WR_wfis1MS61j0R_AjgXuVufmmC0F7R9qSWfR8ft0CbQgemEHY3ddKeW7T7fKv1jnRwYAkl5B_xtvxRFIYT-uR9NNftixNpUIW7q8qvOH7D9icXOg4_wIVxTRe5QiRYwEFoUbV1V9bFtu5FLal0vZnLaWwg5tA6enhzBpxJNdrS0v1RcPpyeNP-9r3cUDGmeftwz9v95UQ","e":"AQAB"}` //nolint + jwkKeyMask2 = `{"kid":"vsR8NCNV_1_LB06LqudGa2r-T0y4Z6VQVYue9IQz6A4","kty":"RSA","alg":"A256GCM","n":"v2KAzzfruqcrVHaE9WSCWIg1xAhMwxTIK-i56WNqPtpWBo9AqxcVea8NyVctEjUNq_mix5CklNy3ru7ARh7rBG_LU65fzs4fY_uYalul3QZSnr61Gj-cTUB3Gy4PhA63yXCbYRR3gDy6WR_wfis1MS61j0R_AjgXuVufmmC0F7R9qSWfR8ft0CbQgemEHY3ddKeW7T7fKv1jnRwYAkl5B_xtvxRFIYT-uR9NNftixNpUIW7q8qvOH7D9icXOg4_wIVxTRe5QiRYwEFoUbV1V9bFtu5FLal0vZnLaWwg5tA6enhzBpxJNdrS0v1RcPpyeNP-9r3cUDGmeftwz9v95UQ","e":"AQAB"}` //nolint +) + +func Test_Sync_EnvVar_Masking(t *testing.T) { + runWhenKongOrKonnect(t, ">=3.1.0") + setup(t) + + t.Run("certificate rotation with masked output", func(t *testing.T) { + reset(t) + + t.Setenv("DECK_CLIENT_CERT", certAMask) + t.Setenv("DECK_CLIENT_KEY", keyAMask) + + _, err := syncWithOutput(context.Background(), "testdata/sync/057-env-var-masking-tests/pem-key.yaml") + require.NoError(t, err) + + // Rotate to different cert (Cert B) + t.Setenv("DECK_CLIENT_CERT", certBMask) + t.Setenv("DECK_CLIENT_KEY", keyBMask) + + output, err := syncWithOutput(context.Background(), "testdata/sync/057-env-var-masking-tests/pem-key.yaml") + require.NoError(t, err) + + fmt.Println(output) + + // Verify that secrets are masked in the output + require.Contains(t, output, `"cert": "[masked]"`, "cert should be masked in sync output") + require.Contains(t, output, `"key": "[masked]"`, "key should be masked in sync output") + + // Verify that actual secret content is NOT exposed + require.NotContains(t, output, "-----BEGIN", "actual cert/key content should not be exposed") + }) + + t.Run("jwk key with masked output", func(t *testing.T) { + reset(t) + t.Setenv("DECK_JWK_KEY", jwkKeyMask) + + _, err := syncWithOutput(context.Background(), "testdata/sync/057-env-var-masking-tests/jwk-key.yaml") + require.NoError(t, err) + + t.Setenv("DECK_JWK_KEY", jwkKeyMask2) + + output, err := syncWithOutput(context.Background(), "testdata/sync/057-env-var-masking-tests/jwk-key.yaml") + require.NoError(t, err) + + fmt.Println(output) + + // Verify that secrets are masked in the output + require.Contains(t, output, `"jwk": "[masked]"`, "jwk should be masked in sync output") + + // Verify that actual secret content is NOT exposed + require.NotContains(t, output, "\"kty\"", "actual jwk content should not be exposed") + }) +} diff --git a/tests/integration/testdata/sync/057-env-var-masking-tests/jwk-key.yaml b/tests/integration/testdata/sync/057-env-var-masking-tests/jwk-key.yaml new file mode 100644 index 000000000..122199392 --- /dev/null +++ b/tests/integration/testdata/sync/057-env-var-masking-tests/jwk-key.yaml @@ -0,0 +1,11 @@ +_format_version: "3.0" +key_sets: +- id: d46b0e15-ffbc-4b15-ad92-09ef67935453 + name: test-keyset +keys: +- id: f21a7073-1183-4b1c-bd87-4d5b8b18eeb4 + jwk: '${{ env "DECK_JWK_KEY" }}' + kid: vsR8NCNV_1_LB06LqudGa2r-T0y4Z6VQVYue9IQz6A4 + set: + id: d46b0e15-ffbc-4b15-ad92-09ef67935453 + name: test-jwk-key diff --git a/tests/integration/testdata/sync/057-env-var-masking-tests/pem-key.yaml b/tests/integration/testdata/sync/057-env-var-masking-tests/pem-key.yaml new file mode 100644 index 000000000..0bccc3e96 --- /dev/null +++ b/tests/integration/testdata/sync/057-env-var-masking-tests/pem-key.yaml @@ -0,0 +1,6 @@ +_format_version: "3.0" +certificates: + - id: 4803cc0f-4155-4802-9b55-ad988c7b0db7 + cert: ${{ env "DECK_CLIENT_CERT" }} + key: ${{ env "DECK_CLIENT_KEY" }} +