diff --git a/docs/modules/azure.md b/docs/modules/azure.md
index c0cc6ca014..a6fc800771 100644
--- a/docs/modules/azure.md
+++ b/docs/modules/azure.md
@@ -24,6 +24,7 @@ The Azure module exposes the following Go packages:
!!! warning "EULA Acceptance"
Due to licensing restrictions you are required to explicitly accept an End User License Agreement (EULA) for the EventHubs container image. This is facilitated through the `WithAcceptEULA` function.
- [CosmosDB](#cosmosdb): `github.com/testcontainers/testcontainers-go/modules/azure/cosmosdb`.
+- [Lowkey Vault](#lowkey-vault): `github.com/testcontainers/testcontainers-go/modules/azure/lowkeyvault`.
[Creating a Azurite container](../../modules/azure/azurite/examples_test.go) inside_block:runAzuriteContainer
@@ -353,3 +354,137 @@ Returns the connection string to connect to the CosmosDB container and an error,
[Connect_CreateDatabase](../../modules/azure/cosmosdb/examples_test.go) inside_block:ExampleRun_connect
+
+## Lowkey Vault
+
+### Run function
+
+- Not available until the next release :material-tag: main
+
+The Lowkey Vault module exposes one entrypoint function to create the Lowkey Vault container, and this function receives three parameters:
+
+```golang
+func Run(ctx context.Context, img string, opts ...testcontainers.ContainerCustomizer) (*Container, error)
+```
+
+- `context.Context`, the Go context.
+- `string`, the Docker image to use.
+- `testcontainers.ContainerCustomizer`, a variadic argument for passing options.
+
+
+#### Image
+
+Use the second argument in the `Run` function to set a valid Docker image.
+In example: `Run(context.Background(), "nagyesta/lowkey-vault:7.0.9-ubi10-minimal")`.
+
+### Container Options
+
+The Lowkey Vault container exposes two ports, one for the Key Vault API and one for the metadata endpoints such as the token endpoint.
+Since the Key Vault API supports multiple vaults and selects the active vault based on the host authority of the request URL, the
+container can be configured in two ways:
+
+#### Local mode
+
+- Not available until the next release :material-tag: main
+
+The default mode is to run the Key Vault container on localhost, meaning that both the Key Vault API and the metadata endpoints are
+exposed using random host ports, and the container is accessible only from the host machine. The default vault is automatically created
+and is made available using the host address.
+
+#### Network mode
+
+- Not available until the next release :material-tag: main
+
+To prepare the container for running in a network and making it accessible from other containers in the network, you can use the
+`WithNetworkAlias` option. For example:
+```golang
+Run(ctx, "nagyesta/lowkey-vault:7.0.9-ubi10-minimal",
+ lowkeyvault.WithNetworkAlias("lowkey-vault", aNetwork),
+)
+```
+
+### Container Methods
+
+The Lowkey Vault container exposes the following methods:
+
+#### ConnectionURL
+
+- Not available until the next release :material-tag: main
+
+Returns the connection URL to connect to the Key Vault API of the Lowkey Vault container and an error, passing the Go context and an
+`accessMode` as parameters. The access mode can be either `lowkeyvault.Local` or `lowkeyvault.Network` depending on the mode you wish
+to use to connect to the Key Vault API.
+
+#### IdentityEndpoint
+
+- Not available until the next release :material-tag: main
+
+Returns the URL pointing to the token endpoint of the Lowkey Vault container and an error, passing the Go context and an `accessMode`
+as parameters. The access mode can be either `lowkeyvault.Local` or `lowkeyvault.Network` depending on the mode you wish to use to
+access the token endpoint. This can be set as the value of the `IDENTITY_ENDPOINT` environment variable without modification on the
+machine which will connect to the Lowkey Vault container. When using the Azure Key Vault SDK for Go, this will let you authenticate
+with managed identities by using the `azidentity.NewDefaultAzureCredential(nil)` as a credential.
+
+#### IdentityHeader
+
+- Not available until the next release :material-tag: main
+
+Can return the string which must be set as the value of the `IDENTITY_HEADER` environment variable without modification on the
+machine which will connect to the Lowkey Vault container. When using the Azure Key Vault SDK for Go, this will let you authenticate
+with managed identities by using the `azidentity.NewDefaultAzureCredential(nil)` as a credential.
+
+#### Client
+
+- Not available until the next release :material-tag: main
+
+Returns a `http.Client` and requires the Go context as parameter. This method can be used to prepare a `http.Client` for connecting
+to the Key Vault API of the Lowkey Vault container using a self-signed certificate. This is necessary since the Lowkey Vault container
+uses a self-signed certificate by default.
+
+### Examples
+
+#### Use the Secrets API in Local mode
+
+In the following example, we are starting the Lowkey Vault container in Local mode, set a secret and retrieve it using the Key Vault Secrets API.
+
+
+[Run Lowkey Vault Container in Local mode](../../modules/azure/lowkeyvault/examples_test.go) inside_block:createContainerWithLocalMode
+[Create Client](../../modules/azure/lowkeyvault/examples_test.go) inside_block:prepareTheSecretClient
+[Set and get a secret](../../modules/azure/lowkeyvault/examples_test.go) inside_block:setAndFetchTheSecret
+
+
+#### Use the Keys API in Local mode
+
+In the following example, we are starting the Lowkey Vault container in Local mode, create a key and encrypt and decrypt a message with it.
+
+
+[Run Lowkey Vault Container in Local mode](../../modules/azure/lowkeyvault/examples_test.go) inside_block:createContainerWithLocalMode
+[Create Client](../../modules/azure/lowkeyvault/examples_test.go) inside_block:prepareTheKeyClient
+[Create a key](../../modules/azure/lowkeyvault/examples_test.go) inside_block:createKey
+[Encrypt a message with the key](../../modules/azure/lowkeyvault/examples_test.go) inside_block:encryptMessage
+[Decrypt cipher text with the key](../../modules/azure/lowkeyvault/examples_test.go) inside_block:decryptCipherText
+
+
+#### Use the Certificates API in Local mode
+
+In the following example, we are starting the Lowkey Vault container in Local mode, create a certificate using the Key Vault Certificates
+API, and fetch the content of the certificate as a PKCS12 store using the Key Vault Secrets API.
+
+
+[Run Lowkey Vault Container in Local mode](../../modules/azure/lowkeyvault/examples_test.go) inside_block:createContainerWithLocalMode
+[Create Certificate Client](../../modules/azure/lowkeyvault/examples_test.go) inside_block:prepareTheCertClient
+[Create a certificate](../../modules/azure/lowkeyvault/examples_test.go) inside_block:createCertificate
+[Create Secret Client](../../modules/azure/lowkeyvault/examples_test.go) inside_block:prepareTheSecretClient
+[Fetch the certificate details](../../modules/azure/lowkeyvault/examples_test.go) inside_block:fetchCertDetails
+
+
+#### Use the Secrets API in Network mode
+
+In the following example, we are starting the Lowkey Vault container in Network mode and setting the parameters of a Go client
+container which will be used to connect to the Key Vault API of the Lowkey Vault container in Network mode.
+
+
+[Run Lowkey Vault Container in Network mode](../../modules/azure/lowkeyvault/examples_test.go) inside_block:createContainerWithNetwork
+[Get the endpoint details in Network mode](../../modules/azure/lowkeyvault/examples_test.go) inside_block:obtainEndpointUrls
+[Configure the client container](../../modules/azure/lowkeyvault/examples_test.go) inside_block:configureClient
+
\ No newline at end of file
diff --git a/modules/azure/go.mod b/modules/azure/go.mod
index dcabfc1a8d..980dc62788 100644
--- a/modules/azure/go.mod
+++ b/modules/azure/go.mod
@@ -5,25 +5,33 @@ go 1.24.0
toolchain go1.24.7
require (
- github.com/Azure/azure-sdk-for-go/sdk/azcore v1.19.1
+ github.com/Azure/azure-sdk-for-go/sdk/azcore v1.21.0
+ github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.13.1
github.com/Azure/azure-sdk-for-go/sdk/data/azcosmos v1.4.1
github.com/Azure/azure-sdk-for-go/sdk/data/aztables v1.3.0
github.com/Azure/azure-sdk-for-go/sdk/messaging/azeventhubs v1.3.0
github.com/Azure/azure-sdk-for-go/sdk/messaging/azservicebus v1.8.0
+ github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/azcertificates v1.4.0
+ github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/azkeys v1.4.0
+ github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/azsecrets v1.4.0
github.com/Azure/azure-sdk-for-go/sdk/storage/azblob v1.6.0
github.com/Azure/azure-sdk-for-go/sdk/storage/azqueue v1.0.0
github.com/docker/go-connections v0.6.0
github.com/stretchr/testify v1.11.1
github.com/testcontainers/testcontainers-go v0.40.0
github.com/testcontainers/testcontainers-go/modules/mssql v0.40.0
+ golang.org/x/crypto v0.47.0
+ software.sslmate.com/src/go-pkcs12 v0.7.0
)
require (
dario.cat/mergo v1.0.2 // indirect
github.com/Azure/azure-sdk-for-go v68.0.0+incompatible // indirect
github.com/Azure/azure-sdk-for-go/sdk/internal v1.11.2 // indirect
+ github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/internal v1.2.0 // indirect
github.com/Azure/go-amqp v1.3.0 // indirect
github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1 // indirect
+ github.com/AzureAD/microsoft-authentication-library-for-go v1.6.0 // indirect
github.com/Microsoft/go-winio v0.6.2 // indirect
github.com/cenkalti/backoff/v4 v4.3.0 // indirect
github.com/containerd/errdefs v1.0.0 // indirect
@@ -40,9 +48,11 @@ require (
github.com/go-logr/logr v1.4.2 // indirect
github.com/go-logr/stdr v1.2.2 // indirect
github.com/go-ole/go-ole v1.2.6 // indirect
+ github.com/golang-jwt/jwt/v5 v5.3.0 // indirect
github.com/google/uuid v1.6.0 // indirect
github.com/grpc-ecosystem/grpc-gateway/v2 v2.16.0 // indirect
github.com/klauspost/compress v1.18.0 // indirect
+ github.com/kylelemons/godebug v1.1.0 // indirect
github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0 // indirect
github.com/magiconair/properties v1.8.10 // indirect
github.com/moby/docker-image-spec v1.3.1 // indirect
@@ -55,6 +65,7 @@ require (
github.com/morikuni/aec v1.0.0 // indirect
github.com/opencontainers/go-digest v1.0.0 // indirect
github.com/opencontainers/image-spec v1.1.1 // indirect
+ github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c // indirect
github.com/pkg/errors v0.9.1 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55 // indirect
@@ -68,10 +79,9 @@ require (
go.opentelemetry.io/otel v1.35.0 // indirect
go.opentelemetry.io/otel/metric v1.35.0 // indirect
go.opentelemetry.io/otel/trace v1.35.0 // indirect
- golang.org/x/crypto v0.45.0 // indirect
- golang.org/x/net v0.47.0 // indirect
- golang.org/x/sys v0.39.0 // indirect
- golang.org/x/text v0.31.0 // indirect
+ golang.org/x/net v0.49.0 // indirect
+ golang.org/x/sys v0.40.0 // indirect
+ golang.org/x/text v0.33.0 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
)
diff --git a/modules/azure/go.sum b/modules/azure/go.sum
index 2871a2aab3..ba975f9bf8 100644
--- a/modules/azure/go.sum
+++ b/modules/azure/go.sum
@@ -4,10 +4,12 @@ github.com/AdaLogics/go-fuzz-headers v0.0.0-20240806141605-e8a1dd7889d6 h1:He8af
github.com/AdaLogics/go-fuzz-headers v0.0.0-20240806141605-e8a1dd7889d6/go.mod h1:8o94RPi1/7XTJvwPpRSzSUedZrtlirdB3r9Z20bi2f8=
github.com/Azure/azure-sdk-for-go v68.0.0+incompatible h1:fcYLmCpyNYRnvJbPerq7U0hS+6+I79yEDJBqVNcqUzU=
github.com/Azure/azure-sdk-for-go v68.0.0+incompatible/go.mod h1:9XXNKU+eRnpl9moKnB4QOLf1HestfXbmab5FXxiDBjc=
-github.com/Azure/azure-sdk-for-go/sdk/azcore v1.19.1 h1:5YTBM8QDVIBN3sxBil89WfdAAqDZbyJTgh688DSxX5w=
-github.com/Azure/azure-sdk-for-go/sdk/azcore v1.19.1/go.mod h1:YD5h/ldMsG0XiIw7PdyNhLxaM317eFh5yNLccNfGdyw=
-github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.10.1 h1:B+blDbyVIG3WaikNxPnhPiJ1MThR03b3vKGtER95TP4=
-github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.10.1/go.mod h1:JdM5psgjfBf5fo2uWOZhflPWyDBZ/O/CNAH9CtsuZE4=
+github.com/Azure/azure-sdk-for-go/sdk/azcore v1.21.0 h1:fou+2+WFTib47nS+nz/ozhEBnvU96bKHy6LjRsY4E28=
+github.com/Azure/azure-sdk-for-go/sdk/azcore v1.21.0/go.mod h1:t76Ruy8AHvUAC8GfMWJMa0ElSbuIcO03NLpynfbgsPA=
+github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.13.1 h1:Hk5QBxZQC1jb2Fwj6mpzme37xbCDdNTxU7O9eb5+LB4=
+github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.13.1/go.mod h1:IYus9qsFobWIc2YVwe/WPjcnyCkPKtnHAqUYeebc8z0=
+github.com/Azure/azure-sdk-for-go/sdk/azidentity/cache v0.3.2 h1:yz1bePFlP5Vws5+8ez6T3HWXPmwOK7Yvq8QxDBD3SKY=
+github.com/Azure/azure-sdk-for-go/sdk/azidentity/cache v0.3.2/go.mod h1:Pa9ZNPuoNu/GztvBSKk9J1cDJW6vk/n0zLtV4mgd8N8=
github.com/Azure/azure-sdk-for-go/sdk/data/azcosmos v1.4.1 h1:ToPLhnXvatKVN4ZkcxLOwcXOJhdu4iQl8w0efeuDz9Y=
github.com/Azure/azure-sdk-for-go/sdk/data/azcosmos v1.4.1/go.mod h1:Krtog/7tz27z75TwM5cIS8bxEH4dcBUezcq+kGVeZEo=
github.com/Azure/azure-sdk-for-go/sdk/data/aztables v1.3.0 h1:NnE8y/opvxowwNcSNHubQUiSSEhfk3dmooLGAOmPuKs=
@@ -22,6 +24,14 @@ github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/eventhub/armeventhub v1.3.
github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/eventhub/armeventhub v1.3.0/go.mod h1:TSH7DcFItwAufy0Lz+Ft2cyopExCpxbOxI5SkH4dRNo=
github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/storage/armstorage v1.6.0 h1:PiSrjRPpkQNjrM8H0WwKMnZUdu1RGMtd/LdGKUrOo+c=
github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/storage/armstorage v1.6.0/go.mod h1:oDrbWx4ewMylP7xHivfgixbfGBT6APAwsSoHRKotnIc=
+github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/azcertificates v1.4.0 h1:mtvR5ZXH5Ew6PSONd5lO5OXovWP1E3oAlgC8fpxor2Q=
+github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/azcertificates v1.4.0/go.mod h1:u560+RFVfG0CBPzkXlDW43slESbBAQjgDGi3r6z+wk8=
+github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/azkeys v1.4.0 h1:E4MgwLBGeVB5f2MdcIVD3ELVAWpr+WD6MUe1i+tM/PA=
+github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/azkeys v1.4.0/go.mod h1:Y2b/1clN4zsAoUd/pgNAQHjLDnTis/6ROkUfyob6psM=
+github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/azsecrets v1.4.0 h1:/g8S6wk65vfC6m3FIxJ+i5QDyN9JWwXI8Hb0Img10hU=
+github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/azsecrets v1.4.0/go.mod h1:gpl+q95AzZlKVI3xSoseF9QPrypk0hQqBiJYeB/cR/I=
+github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/internal v1.2.0 h1:nCYfgcSyHZXJI8J0IWE5MsCGlb2xp9fJiXyxWgmOFg4=
+github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/internal v1.2.0/go.mod h1:ucUjca2JtSZboY8IoUqyQyuuXvwbMBVwFOm0vdQPNhA=
github.com/Azure/azure-sdk-for-go/sdk/storage/azblob v1.6.0 h1:UXT0o77lXQrikd1kgwIPQOUect7EoR/+sbP4wQKdzxM=
github.com/Azure/azure-sdk-for-go/sdk/storage/azblob v1.6.0/go.mod h1:cTvi54pg19DoT07ekoeMgE/taAwNtCShVeZqA+Iv2xI=
github.com/Azure/azure-sdk-for-go/sdk/storage/azqueue v1.0.0 h1:lJwNFV+xYjHREUTHJKx/ZF6CJSt9znxmLw9DqSTvyRU=
@@ -30,8 +40,10 @@ github.com/Azure/go-amqp v1.3.0 h1://1rikYhoIQNXJFXyoO/Rlb4+4EkHYfJceNtLlys2/4=
github.com/Azure/go-amqp v1.3.0/go.mod h1:vZAogwdrkbyK3Mla8m/CxSc/aKdnTZ4IbPxl51Y5WZE=
github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1 h1:UQHMgLO+TxOElx5B5HZ4hJQsoJ/PvUvKRhJHDQXO8P8=
github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1/go.mod h1:xomTg63KZ2rFqZQzSB4Vz2SUXa1BpHTVz9L5PTmPC4E=
-github.com/AzureAD/microsoft-authentication-library-for-go v1.4.2 h1:oygO0locgZJe7PpYPXT5A29ZkwJaPqcva7BVeemZOZs=
-github.com/AzureAD/microsoft-authentication-library-for-go v1.4.2/go.mod h1:wP83P5OoQ5p6ip3ScPr0BAq0BvuPAvacpEuSzyouqAI=
+github.com/AzureAD/microsoft-authentication-extensions-for-go/cache v0.1.1 h1:WJTmL004Abzc5wDB5VtZG2PJk5ndYDgVacGqfirKxjM=
+github.com/AzureAD/microsoft-authentication-extensions-for-go/cache v0.1.1/go.mod h1:tCcJZ0uHAmvjsVYzEFivsRTN00oz5BEsRgQHu5JZ9WE=
+github.com/AzureAD/microsoft-authentication-library-for-go v1.6.0 h1:XRzhVemXdgvJqCH0sFfrBUTnUJSBrBf7++ypk+twtRs=
+github.com/AzureAD/microsoft-authentication-library-for-go v1.6.0/go.mod h1:HKpQxkWaGLJ+D/5H8QRpyQXA1eKjxkFlOMwck5+33Jk=
github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY=
github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU=
github.com/cenkalti/backoff/v4 v4.3.0 h1:MyRJ/UdXutAwSAT+s3wNd7MfTIcy71VQueUuFK343L8=
@@ -74,9 +86,8 @@ github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=
github.com/go-ole/go-ole v1.2.6 h1:/Fpf6oFPoeFik9ty7siob0G6Ke8QvQEuVcuChpwXzpY=
github.com/go-ole/go-ole v1.2.6/go.mod h1:pprOEPIfldk/42T2oK7lQ4v4JSDwmV0As9GaiUsvbm0=
-github.com/golang-jwt/jwt v3.2.1+incompatible h1:73Z+4BJcrTC+KczS6WvTPvRGOp1WmfEP4Q1lOd9Z/+c=
-github.com/golang-jwt/jwt/v5 v5.2.2 h1:Rl4B7itRWVtYIHFrSNd7vhTiz9UpLdi6gZhZ3wEeDy8=
-github.com/golang-jwt/jwt/v5 v5.2.2/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk=
+github.com/golang-jwt/jwt/v5 v5.3.0 h1:pv4AsKCKKZuqlgs5sUmn4x8UlGa0kEVt/puTpKx9vvo=
+github.com/golang-jwt/jwt/v5 v5.3.0/go.mod h1:fxCRLWMO43lRc8nhHWY6LGqRcf+1gQWArsqaEUEa5bE=
github.com/golang-sql/civil v0.0.0-20220223132316-b832511892a9 h1:au07oEsX2xN0ktxqI+Sida1w446QrXBRJ0nee3SNZlA=
github.com/golang-sql/civil v0.0.0-20220223132316-b832511892a9/go.mod h1:8vg3r2VgvsThLBIFL93Qb5yWzgyZWhEmBwUJWevAkK0=
github.com/golang-sql/sqlexp v0.1.0 h1:ZCD6MBpcuOVfGVqsEmY5/4FtYiKz6tSyUv9LPEDei6A=
@@ -92,6 +103,8 @@ github.com/grpc-ecosystem/grpc-gateway/v2 v2.16.0 h1:YBftPWNWd4WwGqtY2yeZL2ef8rH
github.com/grpc-ecosystem/grpc-gateway/v2 v2.16.0/go.mod h1:YN5jB8ie0yfIUg6VvR9Kz84aCaG7AsGZnLjhHbUqwPg=
github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0=
github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4=
+github.com/keybase/go-keychain v0.0.1 h1:way+bWYa6lDppZoZcgMbYsvC7GxljxrskdNInRtuthU=
+github.com/keybase/go-keychain v0.0.1/go.mod h1:PdEILRW3i9D8JcdM+FmY6RwkHGnhHxXwkPPMeUgOK1k=
github.com/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo=
github.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYWRCY2AiWywWQ=
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
@@ -172,20 +185,21 @@ go.opentelemetry.io/otel/trace v1.35.0 h1:dPpEfJu1sDIqruz7BHFG3c7528f6ddfSWfFDVt
go.opentelemetry.io/otel/trace v1.35.0/go.mod h1:WUk7DtFp1Aw2MkvqGdwiXYDZZNvA/1J8o6xRXLrIkyc=
go.opentelemetry.io/proto/otlp v1.0.0 h1:T0TX0tmXU8a3CbNXzEKGeU5mIVOdf0oykP+u2lIVU/I=
go.opentelemetry.io/proto/otlp v1.0.0/go.mod h1:Sy6pihPLfYHkr3NkUbEhGHFhINUSI/v80hjKIs5JXpM=
-golang.org/x/crypto v0.45.0 h1:jMBrvKuj23MTlT0bQEOBcAE0mjg8mK9RXFhRH6nyF3Q=
-golang.org/x/crypto v0.45.0/go.mod h1:XTGrrkGJve7CYK7J8PEww4aY7gM3qMCElcJQ8n8JdX4=
-golang.org/x/net v0.47.0 h1:Mx+4dIFzqraBXUugkia1OOvlD6LemFo1ALMHjrXDOhY=
-golang.org/x/net v0.47.0/go.mod h1:/jNxtkgq5yWUGYkaZGqo27cfGZ1c5Nen03aYrrKpVRU=
+golang.org/x/crypto v0.47.0 h1:V6e3FRj+n4dbpw86FJ8Fv7XVOql7TEwpHapKoMJ/GO8=
+golang.org/x/crypto v0.47.0/go.mod h1:ff3Y9VzzKbwSSEzWqJsJVBnWmRwRSHt/6Op5n9bQc4A=
+golang.org/x/net v0.49.0 h1:eeHFmOGUTtaaPSGNmjBKpbng9MulQsJURQUAfUwY++o=
+golang.org/x/net v0.49.0/go.mod h1:/ysNB2EvaqvesRkuLAyjI1ycPZlQHM3q01F02UY/MV8=
golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201204225414-ed752295db88/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210616094352-59db8d763f22/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
-golang.org/x/sys v0.39.0 h1:CvCKL8MeisomCi6qNZ+wbb0DN9E5AATixKsvNtMoMFk=
-golang.org/x/sys v0.39.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
-golang.org/x/term v0.37.0 h1:8EGAD0qCmHYZg6J17DvsMy9/wJ7/D/4pV/wfnld5lTU=
-golang.org/x/term v0.37.0/go.mod h1:5pB4lxRNYYVZuTLmy8oR2BH8dflOR+IbTYFD8fi3254=
-golang.org/x/text v0.31.0 h1:aC8ghyu4JhP8VojJ2lEHBnochRno1sgL6nEi9WGFGMM=
-golang.org/x/text v0.31.0/go.mod h1:tKRAlv61yKIjGGHX/4tP1LTbc13YSec1pxVEWXzfoeM=
+golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.40.0 h1:DBZZqJ2Rkml6QMQsZywtnjnnGvHza6BTfYFWY9kjEWQ=
+golang.org/x/sys v0.40.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
+golang.org/x/term v0.39.0 h1:RclSuaJf32jOqZz74CkPA9qFuVTX7vhLlpfj/IGWlqY=
+golang.org/x/term v0.39.0/go.mod h1:yxzUCTP/U+FzoxfdKmLaA0RV1WgE0VY7hXBwKtY/4ww=
+golang.org/x/text v0.33.0 h1:B3njUFyqtHDUI5jMn1YIr5B0IE2U0qck04r6d4KPAxE=
+golang.org/x/text v0.33.0/go.mod h1:LuMebE6+rBincTi9+xWTY8TztLzKHc/9C1uBCG27+q8=
golang.org/x/time v0.0.0-20220210224613-90d013bbcef8 h1:vVKdlvoWBphwdxWKrFZEuM0kGgGLxUOYcY4U/2Vjg44=
golang.org/x/time v0.0.0-20220210224613-90d013bbcef8/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
@@ -206,3 +220,5 @@ gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gotest.tools/v3 v3.5.2 h1:7koQfIKdy+I8UTetycgUqXWSDwpgv193Ka+qRsmBY8Q=
gotest.tools/v3 v3.5.2/go.mod h1:LtdLGcnqToBH83WByAAi/wiwSFCArdFIUV/xxN4pcjA=
+software.sslmate.com/src/go-pkcs12 v0.7.0 h1:Db8W44cB54TWD7stUFFSWxdfpdn6fZVcDl0w3R4RVM0=
+software.sslmate.com/src/go-pkcs12 v0.7.0/go.mod h1:Qiz0EyvDRJjjxGyUQa2cCNZn/wMyzrRJ/qcDXOQazLI=
diff --git a/modules/azure/lowkeyvault/examples_test.go b/modules/azure/lowkeyvault/examples_test.go
new file mode 100644
index 0000000000..67071a0114
--- /dev/null
+++ b/modules/azure/lowkeyvault/examples_test.go
@@ -0,0 +1,500 @@
+package lowkeyvault_test
+
+import (
+ "context"
+ "crypto/ecdsa"
+ "crypto/elliptic"
+ "encoding/base64"
+ "fmt"
+ "log"
+ "os"
+
+ "github.com/Azure/azure-sdk-for-go/sdk/azcore/cloud"
+ "github.com/Azure/azure-sdk-for-go/sdk/azcore/policy"
+ "github.com/Azure/azure-sdk-for-go/sdk/azcore/to"
+ "github.com/Azure/azure-sdk-for-go/sdk/azcore/tracing"
+ "github.com/Azure/azure-sdk-for-go/sdk/azidentity"
+ "github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/azcertificates"
+ "github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/azkeys"
+ "github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/azsecrets"
+ "software.sslmate.com/src/go-pkcs12"
+
+ "github.com/testcontainers/testcontainers-go"
+ "github.com/testcontainers/testcontainers-go/modules/azure/lowkeyvault"
+ "github.com/testcontainers/testcontainers-go/network"
+ "github.com/testcontainers/testcontainers-go/wait"
+)
+
+func ExampleRun() {
+ ctx := context.Background()
+
+ lowkeyVaultContainer, err := lowkeyvault.Run(ctx, "nagyesta/lowkey-vault:7.0.9-ubi10-minimal")
+ defer func() {
+ if err := testcontainers.TerminateContainer(lowkeyVaultContainer); err != nil {
+ log.Printf("failed to terminate container: %s", err)
+ }
+ }()
+ if err != nil {
+ log.Printf("failed to start container: %s", err)
+ return
+ }
+
+ state, err := lowkeyVaultContainer.State(ctx)
+ if err != nil {
+ log.Printf("failed to get container state: %s", err)
+ return
+ }
+
+ fmt.Println(state.Running)
+
+ // Output:
+ // true
+}
+
+func ExampleRun_secretOperationsNetwork() {
+ ctx := context.Background()
+
+ aNetwork, err := network.New(ctx)
+ defer func() {
+ err := aNetwork.Remove(ctx)
+ if err != nil {
+ log.Printf("failed to remove network: %s", err)
+ return
+ }
+ }()
+ if err != nil {
+ log.Printf("failed to setup network: %s", err)
+ return
+ }
+
+ // createContainerWithNetwork {
+ lowkeyVaultContainer, err := lowkeyvault.Run(ctx, "nagyesta/lowkey-vault:7.0.9-ubi10-minimal",
+ lowkeyvault.WithNetworkAlias("lowkey-vault", aNetwork),
+ )
+ defer func() {
+ if err := testcontainers.TerminateContainer(lowkeyVaultContainer); err != nil {
+ log.Printf("failed to terminate container: %s", err)
+ }
+ }()
+ if err != nil {
+ log.Printf("failed to start container: %s", err)
+ return
+ }
+ // }
+
+ // obtainEndpointUrls {
+ connURL, err := lowkeyVaultContainer.ConnectionURL(ctx, lowkeyvault.Network)
+ if err != nil {
+ log.Printf("failed to get connection url: %s", err)
+ return
+ }
+
+ identityEndpoint, err := lowkeyVaultContainer.IdentityEndpoint(ctx, lowkeyvault.Network)
+ if err != nil {
+ log.Printf("failed to get token url: %s", err)
+ return
+ }
+ // }
+
+ networkContainer, err := testcontainers.Run(ctx, "",
+ testcontainers.WithDockerfile(
+ testcontainers.FromDockerfile{
+ Context: "testdata",
+ Dockerfile: "Dockerfile",
+ KeepImage: false,
+ }),
+ // configureClient {
+ testcontainers.WithEnv(map[string]string{
+ "IDENTITY_ENDPOINT": identityEndpoint,
+ "IDENTITY_HEADER": lowkeyVaultContainer.IdentityHeader(),
+ "CONNECTION_URL": connURL,
+ }),
+ // }
+ network.WithNetwork(nil, aNetwork),
+ testcontainers.WithWaitStrategy(
+ wait.ForLog("true"),
+ ),
+ )
+ defer func() {
+ if err := testcontainers.TerminateContainer(networkContainer); err != nil {
+ log.Printf("failed to terminate container: %s", err)
+ }
+ }()
+ if err != nil {
+ log.Printf("failed to start container: %s", err)
+ return
+ }
+ state, err := networkContainer.State(ctx)
+ if err != nil {
+ log.Printf("failed to get container state: %s", err)
+ return
+ }
+
+ fmt.Println(state.ExitCode == 0)
+
+ // Output:
+ // true
+}
+
+func ExampleRun_secretOperations() {
+ ctx := context.Background()
+
+ // createContainerWithLocalMode {
+ lowkeyVaultContainer, err := lowkeyvault.Run(ctx, "nagyesta/lowkey-vault:7.0.9-ubi10-minimal")
+ defer func() {
+ if err := testcontainers.TerminateContainer(lowkeyVaultContainer); err != nil {
+ log.Printf("failed to terminate container: %s", err)
+ }
+ }()
+ if err != nil {
+ log.Printf("failed to start container: %s", err)
+ return
+ }
+ // }
+
+ // prepareTheSecretClient {
+ connURL, err := lowkeyVaultContainer.ConnectionURL(ctx, lowkeyvault.Local)
+ if err != nil {
+ log.Printf("failed to get connection url: %s", err)
+ return
+ }
+
+ identityEndpoint, err := lowkeyVaultContainer.IdentityEndpoint(ctx, lowkeyvault.Local)
+ if err != nil {
+ log.Printf("failed to get identity endpoint: %s", err)
+ return
+ }
+ err = os.Setenv("IDENTITY_ENDPOINT", identityEndpoint)
+ if err != nil {
+ log.Printf("failed to set managed identity endpoint variable: %s", err)
+ return
+ }
+ err = os.Setenv("IDENTITY_HEADER", lowkeyVaultContainer.IdentityHeader())
+ if err != nil {
+ log.Printf("failed to set managed identity header variable: %s", err)
+ return
+ }
+
+ httpClient, err := lowkeyVaultContainer.Client(ctx)
+ if err != nil {
+ log.Printf("failed to get client: %s", err)
+ return
+ }
+
+ cred, err := azidentity.NewDefaultAzureCredential(nil) // Will use Managed Identity via the Assumed Identity container
+ if err != nil {
+ log.Printf("failed to create credential: %v", err)
+ return
+ }
+ secretClient, err := azsecrets.NewClient(connURL,
+ cred,
+ &azsecrets.ClientOptions{ClientOptions: struct {
+ APIVersion string
+ Cloud cloud.Configuration
+ InsecureAllowCredentialWithHTTP bool
+ Logging policy.LogOptions
+ Retry policy.RetryOptions
+ Telemetry policy.TelemetryOptions
+ TracingProvider tracing.Provider
+ Transport policy.Transporter
+ PerCallPolicies []policy.Policy
+ PerRetryPolicies []policy.Policy
+ }{Transport: &httpClient}, DisableChallengeResourceVerification: true})
+ if err != nil {
+ log.Printf("failed to create secret client: %v", err)
+ return
+ }
+ // }
+
+ // setAndFetchTheSecret {
+ secretName := "secret-name"
+ secretValue := "a secret value"
+ created, err := secretClient.SetSecret(ctx, secretName, azsecrets.SetSecretParameters{Value: &secretValue}, nil)
+ if err != nil {
+ log.Printf("failed to set the secret %s", err.Error())
+ return
+ }
+
+ fetched, err := secretClient.GetSecret(ctx, secretName, created.ID.Version(), nil)
+ if err != nil {
+ log.Printf("failed to get the secret %s", err.Error())
+ return
+ }
+ fetchedValue := *fetched.Value
+ // }
+
+ fmt.Println(fetchedValue == secretValue)
+
+ // Output:
+ // true
+}
+
+func ExampleRun_keyOperations() {
+ ctx := context.Background()
+
+ lowkeyVaultContainer, err := lowkeyvault.Run(ctx, "nagyesta/lowkey-vault:7.0.9-ubi10-minimal")
+ defer func() {
+ if err := testcontainers.TerminateContainer(lowkeyVaultContainer); err != nil {
+ log.Printf("failed to terminate container: %s", err)
+ }
+ }()
+ if err != nil {
+ log.Printf("failed to start container: %s", err)
+ return
+ }
+
+ // prepareTheKeyClient {
+ connURL, err := lowkeyVaultContainer.ConnectionURL(ctx, lowkeyvault.Local)
+ if err != nil {
+ log.Printf("failed to get connection url: %s", err)
+ return
+ }
+
+ identityEndpoint, err := lowkeyVaultContainer.IdentityEndpoint(ctx, lowkeyvault.Local)
+ if err != nil {
+ log.Printf("failed to get identity endpoint: %s", err)
+ return
+ }
+ err = os.Setenv("IDENTITY_ENDPOINT", identityEndpoint)
+ if err != nil {
+ log.Printf("failed to set managed identity endpoint variable: %s", err)
+ return
+ }
+ err = os.Setenv("IDENTITY_HEADER", lowkeyVaultContainer.IdentityHeader())
+ if err != nil {
+ log.Printf("failed to set managed identity header variable: %s", err)
+ return
+ }
+
+ httpClient, err := lowkeyVaultContainer.Client(ctx)
+ if err != nil {
+ log.Printf("failed to get client: %s", err)
+ return
+ }
+
+ cred, err := azidentity.NewDefaultAzureCredential(nil) // Will use Managed Identity via the Assumed Identity container
+ if err != nil {
+ log.Printf("failed to create credential: %v", err)
+ return
+ }
+ keyClient, err := azkeys.NewClient(connURL,
+ cred,
+ &azkeys.ClientOptions{ClientOptions: struct {
+ APIVersion string
+ Cloud cloud.Configuration
+ InsecureAllowCredentialWithHTTP bool
+ Logging policy.LogOptions
+ Retry policy.RetryOptions
+ Telemetry policy.TelemetryOptions
+ TracingProvider tracing.Provider
+ Transport policy.Transporter
+ PerCallPolicies []policy.Policy
+ PerRetryPolicies []policy.Policy
+ }{Transport: &httpClient}, DisableChallengeResourceVerification: true})
+ if err != nil {
+ log.Printf("failed to create key client: %v", err)
+ return
+ }
+ // }
+
+ // createKey {
+ keyName := "rsa-key"
+ rsaKeyParams := azkeys.CreateKeyParameters{
+ Kty: to.Ptr(azkeys.KeyTypeRSA),
+ KeySize: to.Ptr(int32(2048)),
+ KeyOps: []*azkeys.KeyOperation{
+ to.Ptr(azkeys.KeyOperationDecrypt),
+ to.Ptr(azkeys.KeyOperationEncrypt),
+ to.Ptr(azkeys.KeyOperationUnwrapKey),
+ to.Ptr(azkeys.KeyOperationWrapKey),
+ },
+ }
+ createdKey, err := keyClient.CreateKey(ctx, keyName, rsaKeyParams, nil)
+ if err != nil {
+ log.Printf("failed to create a key: %v", err)
+ return
+ }
+ // }
+
+ // encryptMessage {
+ secretMessage := "a secret message"
+ encryptionParameters := azkeys.KeyOperationParameters{
+ Value: []byte(secretMessage),
+ Algorithm: to.Ptr(azkeys.EncryptionAlgorithmRSAOAEP256),
+ }
+ encrResp, err := keyClient.Encrypt(ctx, keyName, createdKey.Key.KID.Version(), encryptionParameters, nil)
+ if err != nil {
+ log.Printf("failed to encrypt a message: %v", err)
+ return
+ }
+ cipherText := encrResp.Result
+ // }
+
+ // decryptCipherText {
+ decryptionParameters := azkeys.KeyOperationParameters{
+ Value: cipherText,
+ Algorithm: to.Ptr(azkeys.EncryptionAlgorithmRSAOAEP256),
+ }
+ decrResp, err := keyClient.Decrypt(ctx, keyName, createdKey.Key.KID.Version(), decryptionParameters, nil)
+ if err != nil {
+ log.Printf("failed to decrypt a message: %v", err)
+ return
+ }
+ decryptedMessage := string(decrResp.Result)
+ // }
+
+ fmt.Println(decryptedMessage == secretMessage)
+
+ // Output:
+ // true
+}
+
+func ExampleRun_certificateOperations() {
+ ctx := context.Background()
+
+ lowkeyVaultContainer, err := lowkeyvault.Run(ctx, "nagyesta/lowkey-vault:7.0.9-ubi10-minimal")
+ defer func() {
+ if err := testcontainers.TerminateContainer(lowkeyVaultContainer); err != nil {
+ log.Printf("failed to terminate container: %s", err)
+ }
+ }()
+ if err != nil {
+ log.Printf("failed to start container: %s", err)
+ return
+ }
+
+ // prepareTheCertClient {
+ connURL, err := lowkeyVaultContainer.ConnectionURL(ctx, lowkeyvault.Local)
+ if err != nil {
+ log.Printf("failed to get connection url: %s", err)
+ return
+ }
+
+ identityEndpoint, err := lowkeyVaultContainer.IdentityEndpoint(ctx, lowkeyvault.Local)
+ if err != nil {
+ log.Printf("failed to get identity endpoint: %s", err)
+ return
+ }
+ err = os.Setenv("IDENTITY_ENDPOINT", identityEndpoint)
+ if err != nil {
+ log.Printf("failed to set managed identity endpoint variable: %s", err)
+ return
+ }
+ err = os.Setenv("IDENTITY_HEADER", lowkeyVaultContainer.IdentityHeader())
+ if err != nil {
+ log.Printf("failed to set managed identity header variable: %s", err)
+ return
+ }
+
+ httpClient, err := lowkeyVaultContainer.Client(ctx)
+ if err != nil {
+ log.Printf("failed to get client: %s", err)
+ return
+ }
+
+ cred, err := azidentity.NewDefaultAzureCredential(nil) // Will use Managed Identity via the Assumed Identity container
+ if err != nil {
+ log.Printf("failed to create credential: %v", err)
+ return
+ }
+ certClient, err := azcertificates.NewClient(connURL,
+ cred,
+ &azcertificates.ClientOptions{ClientOptions: struct {
+ APIVersion string
+ Cloud cloud.Configuration
+ InsecureAllowCredentialWithHTTP bool
+ Logging policy.LogOptions
+ Retry policy.RetryOptions
+ Telemetry policy.TelemetryOptions
+ TracingProvider tracing.Provider
+ Transport policy.Transporter
+ PerCallPolicies []policy.Policy
+ PerRetryPolicies []policy.Policy
+ }{Transport: &httpClient}, DisableChallengeResourceVerification: true})
+ if err != nil {
+ log.Printf("failed to create certificate client: %v", err)
+ return
+ }
+ // }
+
+ // createCertificate {
+ certName := "ec-cert"
+ subject := "CN=example.com"
+ _, err = certClient.CreateCertificate(ctx, certName, azcertificates.CreateCertificateParameters{
+ CertificatePolicy: &azcertificates.CertificatePolicy{
+ IssuerParameters: &azcertificates.IssuerParameters{
+ Name: to.Ptr("Self"),
+ },
+ KeyProperties: &azcertificates.KeyProperties{
+ Curve: to.Ptr(azcertificates.CurveNameP256),
+ KeyType: to.Ptr(azcertificates.KeyTypeEC),
+ ReuseKey: to.Ptr(true),
+ },
+ SecretProperties: &azcertificates.SecretProperties{
+ ContentType: to.Ptr("application/x-pkcs12"),
+ },
+ X509CertificateProperties: &azcertificates.X509CertificateProperties{
+ Subject: &subject,
+ SubjectAlternativeNames: &azcertificates.SubjectAlternativeNames{
+ DNSNames: []*string{to.Ptr("localhost")},
+ },
+ ValidityInMonths: to.Ptr(int32(12)),
+ },
+ },
+ }, nil)
+ if err != nil {
+ log.Printf("failed to create a certificate: %v", err)
+ return
+ }
+ // }
+
+ secretClient, err := azsecrets.NewClient(connURL,
+ cred,
+ &azsecrets.ClientOptions{ClientOptions: struct {
+ APIVersion string
+ Cloud cloud.Configuration
+ InsecureAllowCredentialWithHTTP bool
+ Logging policy.LogOptions
+ Retry policy.RetryOptions
+ Telemetry policy.TelemetryOptions
+ TracingProvider tracing.Provider
+ Transport policy.Transporter
+ PerCallPolicies []policy.Policy
+ PerRetryPolicies []policy.Policy
+ }{Transport: &httpClient}, DisableChallengeResourceVerification: true})
+ if err != nil {
+ log.Printf("failed to create secret client: %v", err)
+ return
+ }
+
+ // fetchCertDetails {
+ base64Secret, err := secretClient.GetSecret(ctx, certName, "", nil)
+ if err != nil {
+ log.Printf("failed to get the secret with certificate store: %v", err)
+ return
+ }
+ base64Value := *base64Secret.Value
+ bytes, err := base64.StdEncoding.DecodeString(base64Value)
+ if err != nil {
+ log.Printf("failed to decode the certificate store: %v", err)
+ return
+ }
+ // use SSLMate library to decode the certificate store as the x/crypto
+ // library is not fully compatible with the Java PKCS12 format
+ key, cert, err := pkcs12.Decode(bytes, "")
+ if err != nil {
+ log.Printf("failed to open certificate store: %v", err)
+ return
+ }
+ ecKey, ok := key.(*ecdsa.PrivateKey)
+ if !ok {
+ log.Printf("unexpected key type: %T", key)
+ return
+ }
+ // }
+
+ fmt.Println(cert.Subject.String() == subject && ecKey.Curve == elliptic.P256())
+
+ // Output:
+ // true
+}
diff --git a/modules/azure/lowkeyvault/lowkeyvault.go b/modules/azure/lowkeyvault/lowkeyvault.go
new file mode 100644
index 0000000000..689a96a484
--- /dev/null
+++ b/modules/azure/lowkeyvault/lowkeyvault.go
@@ -0,0 +1,230 @@
+package lowkeyvault
+
+import (
+ "context"
+ "crypto/tls"
+ "crypto/x509"
+ "errors"
+ "fmt"
+ "io"
+ "net/http"
+
+ "github.com/docker/go-connections/nat"
+ "golang.org/x/crypto/pkcs12"
+
+ "github.com/testcontainers/testcontainers-go"
+ "github.com/testcontainers/testcontainers-go/network"
+ "github.com/testcontainers/testcontainers-go/wait"
+)
+
+// possible access modes
+const (
+ Local = iota
+ Network
+)
+
+const (
+ // defaultAPIPort is the default port used by for the Lowkey Vault Key Vault API endpoints
+ defaultAPIPort nat.Port = "8443/tcp"
+ // defaultMetadataPort is the default port used for the Lowkey Vault Metadata endpoints
+ defaultMetadataPort nat.Port = "8080/tcp"
+)
+
+// Container represents the Lowkey Vault container type used in the module
+type Container struct {
+ testcontainers.Container
+ localHostName string
+ remoteHostName string
+}
+
+// WithNetworkAlias sets the alias of the container for the provided network and adds the specified name as a key vault alias for the default, "localhost", vault.
+func WithNetworkAlias(alias string, forNetwork *testcontainers.DockerNetwork) testcontainers.CustomizeRequestOption {
+ return func(req *testcontainers.GenericContainerRequest) error {
+ err := network.WithNetwork([]string{alias}, forNetwork).Customize(req)
+ if err != nil {
+ return err
+ }
+ // The placeholder will be replaced by the container automatically just in time
+ envs := map[string]string{"LOWKEY_VAULT_ALIASES": "localhost=" + alias + ":"}
+ err = testcontainers.WithEnv(envs).Customize(req)
+ if err != nil {
+ return err
+ }
+ return nil
+ }
+}
+
+// Run creates an instance of the Lowkey Vault container type
+func Run(ctx context.Context, img string, opts ...testcontainers.ContainerCustomizer) (*Container, error) {
+ // Initialize with module defaults
+ moduleOpts := []testcontainers.ContainerCustomizer{
+ testcontainers.WithExposedPorts(defaultAPIPort.Port(), defaultMetadataPort.Port()),
+ testcontainers.WithEnv(map[string]string{
+ "LOWKEY_VAULT_RELAXED_PORTS": "true",
+ }),
+ testcontainers.WithWaitStrategy(
+ wait.ForLog("Started LowkeyVaultApp in "),
+ ),
+ }
+
+ // Add user-provided options
+ moduleOpts = append(moduleOpts, opts...)
+
+ ctr, err := testcontainers.Run(ctx, img, moduleOpts...)
+ var c *Container
+ if ctr != nil {
+ c = &Container{Container: ctr, localHostName: "", remoteHostName: ""}
+ }
+
+ if err != nil {
+ return c, fmt.Errorf("run lowkeyvault: %w", err)
+ }
+
+ return c, nil
+}
+
+// Client prepares a client which will accept insecure (self-signed) certificates.
+func (c *Container) Client(ctx context.Context) (http.Client, error) {
+ customTransport := http.DefaultTransport.(*http.Transport).Clone()
+ authority, err := c.mappedHostAuthority(ctx, defaultMetadataPort, Local)
+ if err != nil {
+ return http.Client{}, fmt.Errorf("host authority: %w", err)
+ }
+ pass, err := c.fetchDefaultCertPassword(authority)
+ if err != nil {
+ return http.Client{}, fmt.Errorf("default cert password: %w", err)
+ }
+ p12Data, err := c.fetchDefaultCertContent(authority)
+ if err != nil {
+ return http.Client{}, fmt.Errorf("default cert content: %w", err)
+ }
+
+ _, cert, err := pkcs12.Decode(p12Data, pass)
+ if err != nil {
+ return http.Client{}, fmt.Errorf("decode cert content: %w", err)
+ }
+
+ certPool := x509.NewCertPool()
+ certPool.AddCert(cert)
+
+ customTransport.TLSClientConfig = &tls.Config{RootCAs: certPool}
+ return http.Client{Transport: customTransport}, nil
+}
+
+func (c *Container) fetchDefaultCertPassword(authority string) (string, error) {
+ bytes, err := c.fetchContent("http://" + authority + "/metadata/default-cert/password")
+ if err != nil {
+ return "", fmt.Errorf("default cert password: %w", err)
+ }
+ return string(bytes), nil
+}
+
+func (c *Container) fetchDefaultCertContent(authority string) ([]byte, error) {
+ return c.fetchContent("http://" + authority + "/metadata/default-cert/lowkey-vault.p12")
+}
+
+func (c *Container) fetchContent(url string) ([]byte, error) {
+ resp, err := http.Get(url)
+ if resp != nil {
+ defer resp.Body.Close()
+ }
+ if err != nil {
+ return nil, fmt.Errorf("get content: %w", err)
+ }
+
+ if resp != nil && resp.StatusCode == http.StatusOK {
+ bodyBytes, err := io.ReadAll(resp.Body)
+ if err != nil {
+ return nil, fmt.Errorf("read response: %w", err)
+ }
+ return bodyBytes, nil
+ }
+
+ return nil, errors.New("content not found")
+}
+
+// ConnectionURL returns the connection URL for the Lowkey Vault API based on the provided access mode.
+func (c *Container) ConnectionURL(ctx context.Context, accessMode int) (string, error) {
+ hostAuthority, err := c.mappedHostAuthority(ctx, defaultAPIPort, accessMode)
+ if err != nil {
+ return "", fmt.Errorf("host authority: %w", err)
+ }
+
+ return "https://" + hostAuthority, nil
+}
+
+// IdentityEndpoint returns the URL value of the IDENTITY_ENDPOINT environment variable for the managed identity simulation. This will be used to obtain an access token for the Lowkey Vault API.
+func (c *Container) IdentityEndpoint(ctx context.Context, accessMode int) (string, error) {
+ hostAuthority, err := c.mappedHostAuthority(ctx, defaultMetadataPort, accessMode)
+ if err != nil {
+ return "", fmt.Errorf("host authority: %w", err)
+ }
+ return fmt.Sprintf("http://%s/metadata/identity/oauth2/token", hostAuthority), nil
+}
+
+// IdentityHeader returns the value of the IDENTITY_HEADER environment variable for the managed identity simulation.
+func (c *Container) IdentityHeader() string {
+ return "header"
+}
+
+func (c *Container) mappedHostAuthority(ctx context.Context, exposedPort nat.Port, accessMode int) (string, error) {
+ switch accessMode {
+ case Local:
+ host, err := c.resolveLocalHostName(ctx)
+ if err != nil {
+ return "", fmt.Errorf("host: %w", err)
+ }
+ port, err := c.MappedPort(ctx, exposedPort)
+ if err != nil {
+ return "", fmt.Errorf("port: %w", err)
+ }
+ return fmt.Sprintf("%s:%d", host, port.Int()), nil
+ case Network:
+ host, err := c.resolveNetworkHostName(ctx)
+ if err != nil {
+ return "", fmt.Errorf("host: %w", err)
+ }
+ return fmt.Sprintf("%s:%d", host, exposedPort.Int()), nil
+ default:
+ return "", fmt.Errorf("unsupported access mode: %d", accessMode)
+ }
+}
+
+func (c *Container) resolveLocalHostName(ctx context.Context) (string, error) {
+ if c.localHostName != "" {
+ return c.localHostName, nil
+ }
+ host, err := c.Host(ctx)
+ if err != nil {
+ return "", fmt.Errorf("host: %w", err)
+ }
+ c.localHostName = host
+ return host, nil
+}
+
+func (c *Container) resolveNetworkHostName(ctx context.Context) (string, error) {
+ if c.remoteHostName != "" {
+ return c.remoteHostName, nil
+ }
+ networks, err := c.Networks(ctx)
+ if err != nil {
+ return "", fmt.Errorf("networks: %w", err)
+ }
+ if len(networks) != 1 {
+ return "", fmt.Errorf("the container must have exactly one network, but it has %d", len(networks))
+ }
+ hosts, err := c.NetworkAliases(ctx)
+ if err != nil {
+ return "", fmt.Errorf("network aliases: %w", err)
+ }
+ if len(hosts) == 0 {
+ return "", errors.New("no network aliases found in the Lowkey Vault container")
+ }
+ aliases := hosts[networks[0]]
+ if len(aliases) != 1 {
+ return "", fmt.Errorf("the container must have exactly one network alias, but it has %d", len(aliases))
+ }
+ host := aliases[0]
+ c.remoteHostName = host
+ return host, nil
+}
diff --git a/modules/azure/lowkeyvault/lowkeyvault_test.go b/modules/azure/lowkeyvault/lowkeyvault_test.go
new file mode 100644
index 0000000000..8516deab7f
--- /dev/null
+++ b/modules/azure/lowkeyvault/lowkeyvault_test.go
@@ -0,0 +1,306 @@
+package lowkeyvault_test
+
+import (
+ "context"
+ "crypto/ecdsa"
+ "crypto/elliptic"
+ "encoding/base64"
+ "testing"
+
+ "github.com/Azure/azure-sdk-for-go/sdk/azcore/cloud"
+ "github.com/Azure/azure-sdk-for-go/sdk/azcore/policy"
+ "github.com/Azure/azure-sdk-for-go/sdk/azcore/to"
+ "github.com/Azure/azure-sdk-for-go/sdk/azcore/tracing"
+ "github.com/Azure/azure-sdk-for-go/sdk/azidentity"
+ "github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/azcertificates"
+ "github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/azkeys"
+ "github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/azsecrets"
+ "github.com/stretchr/testify/require"
+ "software.sslmate.com/src/go-pkcs12"
+
+ "github.com/testcontainers/testcontainers-go"
+ "github.com/testcontainers/testcontainers-go/modules/azure/lowkeyvault"
+ "github.com/testcontainers/testcontainers-go/network"
+ "github.com/testcontainers/testcontainers-go/wait"
+)
+
+func TestRun(t *testing.T) {
+ ctx := context.Background()
+
+ lowkeyVaultContainer, err := lowkeyvault.Run(ctx, "nagyesta/lowkey-vault:7.0.9-ubi10-minimal")
+ testcontainers.CleanupContainer(t, lowkeyVaultContainer)
+ require.NoError(t, err)
+
+ state, err := lowkeyVaultContainer.State(ctx)
+ require.NoError(t, err)
+
+ require.True(t, state.Running)
+}
+
+func TestRun_secretOperationsNetwork(t *testing.T) {
+ ctx := context.Background()
+
+ aNetwork, err := network.New(ctx)
+ testcontainers.CleanupNetwork(t, aNetwork)
+ require.NoError(t, err)
+
+ lowkeyVaultContainer, err := lowkeyvault.Run(ctx, "nagyesta/lowkey-vault:7.0.9-ubi10-minimal",
+ lowkeyvault.WithNetworkAlias("lowkey-vault", aNetwork),
+ )
+ testcontainers.CleanupContainer(t, lowkeyVaultContainer)
+ require.NoError(t, err)
+
+ connURL, err := lowkeyVaultContainer.ConnectionURL(ctx, lowkeyvault.Network)
+ require.NoError(t, err)
+ require.NotNil(t, connURL)
+
+ tokenURL, err := lowkeyVaultContainer.IdentityEndpoint(ctx, lowkeyvault.Network)
+ require.NoError(t, err)
+ require.NotNil(t, tokenURL)
+
+ networkContainer, err := testcontainers.Run(ctx, "",
+ testcontainers.WithDockerfile(
+ testcontainers.FromDockerfile{
+ Context: "testdata",
+ Dockerfile: "Dockerfile",
+ KeepImage: false,
+ }),
+ testcontainers.WithEnv(map[string]string{
+ "IDENTITY_ENDPOINT": tokenURL,
+ "IDENTITY_HEADER": lowkeyVaultContainer.IdentityHeader(),
+ "CONNECTION_URL": connURL,
+ }),
+ network.WithNetwork(nil, aNetwork),
+ testcontainers.WithWaitStrategy(
+ wait.ForLog("true"),
+ ),
+ )
+ testcontainers.CleanupContainer(t, networkContainer)
+ require.NoError(t, err)
+ require.NotNil(t, networkContainer)
+ state, err := networkContainer.State(ctx)
+ require.NoError(t, err)
+
+ require.Equal(t, 0, state.ExitCode)
+}
+
+func TestRun_secretOperations(t *testing.T) {
+ ctx := context.Background()
+
+ lowkeyVaultContainer, err := lowkeyvault.Run(ctx, "nagyesta/lowkey-vault:7.0.9-ubi10-minimal")
+ testcontainers.CleanupContainer(t, lowkeyVaultContainer)
+ require.NoError(t, err)
+
+ connURL, err := lowkeyVaultContainer.ConnectionURL(ctx, lowkeyvault.Local)
+ require.NoError(t, err)
+ require.NotNil(t, connURL)
+
+ identityEndpoint, err := lowkeyVaultContainer.IdentityEndpoint(ctx, lowkeyvault.Local)
+ require.NoError(t, err)
+ t.Setenv("IDENTITY_ENDPOINT", identityEndpoint)
+ t.Setenv("IDENTITY_HEADER", lowkeyVaultContainer.IdentityHeader())
+
+ httpClient, err := lowkeyVaultContainer.Client(ctx)
+ require.NoError(t, err)
+
+ cred, err := azidentity.NewDefaultAzureCredential(nil) // Will use Managed Identity via the Assumed Identity container
+ require.NoError(t, err)
+ secretClient, err := azsecrets.NewClient(connURL,
+ cred,
+ &azsecrets.ClientOptions{ClientOptions: struct {
+ APIVersion string
+ Cloud cloud.Configuration
+ InsecureAllowCredentialWithHTTP bool
+ Logging policy.LogOptions
+ Retry policy.RetryOptions
+ Telemetry policy.TelemetryOptions
+ TracingProvider tracing.Provider
+ Transport policy.Transporter
+ PerCallPolicies []policy.Policy
+ PerRetryPolicies []policy.Policy
+ }{Transport: &httpClient}, DisableChallengeResourceVerification: true})
+ require.NoError(t, err)
+
+ secretName := "secret-name"
+ secretValue := "a secret value"
+ created, err := secretClient.SetSecret(ctx, secretName, azsecrets.SetSecretParameters{Value: &secretValue}, nil)
+ require.NoError(t, err)
+ require.NotNil(t, created)
+
+ fetched, err := secretClient.GetSecret(ctx, secretName, created.ID.Version(), nil)
+ require.NoError(t, err)
+ require.NotNil(t, fetched)
+ fetchedValue := *fetched.Value
+
+ require.Equal(t, secretValue, fetchedValue)
+}
+
+func TestRun_keyOperations(t *testing.T) {
+ ctx := context.Background()
+
+ lowkeyVaultContainer, err := lowkeyvault.Run(ctx, "nagyesta/lowkey-vault:7.0.9-ubi10-minimal")
+ testcontainers.CleanupContainer(t, lowkeyVaultContainer)
+ require.NoError(t, err)
+
+ connURL, err := lowkeyVaultContainer.ConnectionURL(ctx, lowkeyvault.Local)
+ require.NoError(t, err)
+ require.NotNil(t, connURL)
+
+ identityEndpoint, err := lowkeyVaultContainer.IdentityEndpoint(ctx, lowkeyvault.Local)
+ require.NoError(t, err)
+ t.Setenv("IDENTITY_ENDPOINT", identityEndpoint)
+ t.Setenv("IDENTITY_HEADER", lowkeyVaultContainer.IdentityHeader())
+
+ httpClient, err := lowkeyVaultContainer.Client(ctx)
+ require.NoError(t, err)
+
+ cred, err := azidentity.NewDefaultAzureCredential(nil) // Will use Managed Identity via the Assumed Identity container
+ require.NoError(t, err)
+ keyClient, err := azkeys.NewClient(connURL,
+ cred,
+ &azkeys.ClientOptions{ClientOptions: struct {
+ APIVersion string
+ Cloud cloud.Configuration
+ InsecureAllowCredentialWithHTTP bool
+ Logging policy.LogOptions
+ Retry policy.RetryOptions
+ Telemetry policy.TelemetryOptions
+ TracingProvider tracing.Provider
+ Transport policy.Transporter
+ PerCallPolicies []policy.Policy
+ PerRetryPolicies []policy.Policy
+ }{Transport: &httpClient}, DisableChallengeResourceVerification: true})
+ require.NoError(t, err)
+
+ keyName := "rsa-key"
+ rsaKeyParams := azkeys.CreateKeyParameters{
+ Kty: to.Ptr(azkeys.KeyTypeRSA),
+ KeySize: to.Ptr(int32(2048)),
+ KeyOps: []*azkeys.KeyOperation{
+ to.Ptr(azkeys.KeyOperationDecrypt),
+ to.Ptr(azkeys.KeyOperationEncrypt),
+ to.Ptr(azkeys.KeyOperationUnwrapKey),
+ to.Ptr(azkeys.KeyOperationWrapKey),
+ },
+ }
+ createdKey, err := keyClient.CreateKey(ctx, keyName, rsaKeyParams, nil)
+ require.NoError(t, err)
+ require.NotNil(t, createdKey)
+
+ secretMessage := "a secret message"
+ encryptionParameters := azkeys.KeyOperationParameters{
+ Value: []byte(secretMessage),
+ Algorithm: to.Ptr(azkeys.EncryptionAlgorithmRSAOAEP256),
+ }
+ encrResp, err := keyClient.Encrypt(ctx, keyName, createdKey.Key.KID.Version(), encryptionParameters, nil)
+ require.NoError(t, err)
+ require.NotNil(t, encrResp)
+ cipherText := encrResp.Result
+
+ decryptionParameters := azkeys.KeyOperationParameters{
+ Value: cipherText,
+ Algorithm: to.Ptr(azkeys.EncryptionAlgorithmRSAOAEP256),
+ }
+ decrResp, err := keyClient.Decrypt(ctx, keyName, createdKey.Key.KID.Version(), decryptionParameters, nil)
+ require.NoError(t, err)
+ require.NotNil(t, decrResp)
+ decryptedMessage := string(decrResp.Result)
+
+ require.Equal(t, secretMessage, decryptedMessage)
+}
+
+func TestRun_certificateOperations(t *testing.T) {
+ ctx := context.Background()
+
+ lowkeyVaultContainer, err := lowkeyvault.Run(ctx, "nagyesta/lowkey-vault:7.0.9-ubi10-minimal")
+ testcontainers.CleanupContainer(t, lowkeyVaultContainer)
+ require.NoError(t, err)
+
+ connURL, err := lowkeyVaultContainer.ConnectionURL(ctx, lowkeyvault.Local)
+ require.NoError(t, err)
+ require.NotNil(t, connURL)
+
+ identityEndpoint, err := lowkeyVaultContainer.IdentityEndpoint(ctx, lowkeyvault.Local)
+ require.NoError(t, err)
+ t.Setenv("IDENTITY_ENDPOINT", identityEndpoint)
+ t.Setenv("IDENTITY_HEADER", lowkeyVaultContainer.IdentityHeader())
+
+ httpClient, err := lowkeyVaultContainer.Client(ctx)
+ require.NoError(t, err)
+
+ cred, err := azidentity.NewDefaultAzureCredential(nil) // Will use Managed Identity via the Assumed Identity container
+ require.NoError(t, err)
+ certClient, err := azcertificates.NewClient(connURL,
+ cred,
+ &azcertificates.ClientOptions{ClientOptions: struct {
+ APIVersion string
+ Cloud cloud.Configuration
+ InsecureAllowCredentialWithHTTP bool
+ Logging policy.LogOptions
+ Retry policy.RetryOptions
+ Telemetry policy.TelemetryOptions
+ TracingProvider tracing.Provider
+ Transport policy.Transporter
+ PerCallPolicies []policy.Policy
+ PerRetryPolicies []policy.Policy
+ }{Transport: &httpClient}, DisableChallengeResourceVerification: true})
+ require.NoError(t, err)
+
+ certName := "ec-cert"
+ subject := "CN=example.com"
+ created, err := certClient.CreateCertificate(ctx, certName, azcertificates.CreateCertificateParameters{
+ CertificatePolicy: &azcertificates.CertificatePolicy{
+ IssuerParameters: &azcertificates.IssuerParameters{
+ Name: to.Ptr("Self"),
+ },
+ KeyProperties: &azcertificates.KeyProperties{
+ Curve: to.Ptr(azcertificates.CurveNameP256),
+ KeyType: to.Ptr(azcertificates.KeyTypeEC),
+ ReuseKey: to.Ptr(true),
+ },
+ SecretProperties: &azcertificates.SecretProperties{
+ ContentType: to.Ptr("application/x-pkcs12"),
+ },
+ X509CertificateProperties: &azcertificates.X509CertificateProperties{
+ Subject: &subject,
+ SubjectAlternativeNames: &azcertificates.SubjectAlternativeNames{
+ DNSNames: []*string{to.Ptr("localhost")},
+ },
+ ValidityInMonths: to.Ptr(int32(12)),
+ },
+ },
+ }, nil)
+ require.NoError(t, err)
+ require.NotNil(t, created)
+
+ secretClient, err := azsecrets.NewClient(connURL,
+ cred,
+ &azsecrets.ClientOptions{ClientOptions: struct {
+ APIVersion string
+ Cloud cloud.Configuration
+ InsecureAllowCredentialWithHTTP bool
+ Logging policy.LogOptions
+ Retry policy.RetryOptions
+ Telemetry policy.TelemetryOptions
+ TracingProvider tracing.Provider
+ Transport policy.Transporter
+ PerCallPolicies []policy.Policy
+ PerRetryPolicies []policy.Policy
+ }{Transport: &httpClient}, DisableChallengeResourceVerification: true})
+ require.NoError(t, err)
+
+ base64Secret, err := secretClient.GetSecret(ctx, certName, "", nil)
+ require.NoError(t, err)
+ require.NotNil(t, base64Secret)
+ base64Value := *base64Secret.Value
+ bytes, err := base64.StdEncoding.DecodeString(base64Value)
+ require.NoError(t, err)
+ // use SSLMate library to decode the certificate store as the x/crypto
+ // library is not fully compatible with the Java PKCS12 format
+ key, cert, err := pkcs12.Decode(bytes, "")
+ require.NoError(t, err)
+ ecKey := key.(*ecdsa.PrivateKey)
+
+ require.Equal(t, subject, cert.Subject.String())
+ require.Equal(t, elliptic.P256(), ecKey.Curve)
+}
diff --git a/modules/azure/lowkeyvault/testdata/Dockerfile b/modules/azure/lowkeyvault/testdata/Dockerfile
new file mode 100644
index 0000000000..d0980b51d0
--- /dev/null
+++ b/modules/azure/lowkeyvault/testdata/Dockerfile
@@ -0,0 +1,11 @@
+FROM golang:1.24-alpine@sha256:fc2cff6625f3c1c92e6c85938ac5bd09034ad0d4bc2dfb08278020b68540dbb5 as builder
+WORKDIR /app
+COPY . .
+RUN mkdir -p dist
+RUN go build -o ./dist/server main.go
+
+FROM alpine
+WORKDIR /app
+COPY --from=builder /app/dist/server .
+EXPOSE 6443
+CMD ["/app/server"]
diff --git a/modules/azure/lowkeyvault/testdata/go.mod b/modules/azure/lowkeyvault/testdata/go.mod
new file mode 100644
index 0000000000..db119a2f1f
--- /dev/null
+++ b/modules/azure/lowkeyvault/testdata/go.mod
@@ -0,0 +1,25 @@
+module lowkeyvault_network_test
+
+go 1.24.0
+
+toolchain go1.24.7
+
+require (
+ github.com/Azure/azure-sdk-for-go/sdk/azcore v1.21.0
+ github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.13.1
+ github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/azsecrets v1.4.0
+)
+
+require (
+ github.com/Azure/azure-sdk-for-go/sdk/internal v1.11.2 // indirect
+ github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/internal v1.2.0 // indirect
+ github.com/AzureAD/microsoft-authentication-library-for-go v1.6.0 // indirect
+ github.com/golang-jwt/jwt/v5 v5.3.0 // indirect
+ github.com/google/uuid v1.6.0 // indirect
+ github.com/kylelemons/godebug v1.1.0 // indirect
+ github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c // indirect
+ golang.org/x/crypto v0.47.0 // indirect
+ golang.org/x/net v0.49.0 // indirect
+ golang.org/x/sys v0.40.0 // indirect
+ golang.org/x/text v0.33.0 // indirect
+)
diff --git a/modules/azure/lowkeyvault/testdata/go.sum b/modules/azure/lowkeyvault/testdata/go.sum
new file mode 100644
index 0000000000..f5af262ba7
--- /dev/null
+++ b/modules/azure/lowkeyvault/testdata/go.sum
@@ -0,0 +1,43 @@
+github.com/Azure/azure-sdk-for-go/sdk/azcore v1.21.0 h1:fou+2+WFTib47nS+nz/ozhEBnvU96bKHy6LjRsY4E28=
+github.com/Azure/azure-sdk-for-go/sdk/azcore v1.21.0/go.mod h1:t76Ruy8AHvUAC8GfMWJMa0ElSbuIcO03NLpynfbgsPA=
+github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.13.1 h1:Hk5QBxZQC1jb2Fwj6mpzme37xbCDdNTxU7O9eb5+LB4=
+github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.13.1/go.mod h1:IYus9qsFobWIc2YVwe/WPjcnyCkPKtnHAqUYeebc8z0=
+github.com/Azure/azure-sdk-for-go/sdk/azidentity/cache v0.3.2 h1:yz1bePFlP5Vws5+8ez6T3HWXPmwOK7Yvq8QxDBD3SKY=
+github.com/Azure/azure-sdk-for-go/sdk/azidentity/cache v0.3.2/go.mod h1:Pa9ZNPuoNu/GztvBSKk9J1cDJW6vk/n0zLtV4mgd8N8=
+github.com/Azure/azure-sdk-for-go/sdk/internal v1.11.2 h1:9iefClla7iYpfYWdzPCRDozdmndjTm8DXdpCzPajMgA=
+github.com/Azure/azure-sdk-for-go/sdk/internal v1.11.2/go.mod h1:XtLgD3ZD34DAaVIIAyG3objl5DynM3CQ/vMcbBNJZGI=
+github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/azsecrets v1.4.0 h1:/g8S6wk65vfC6m3FIxJ+i5QDyN9JWwXI8Hb0Img10hU=
+github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/azsecrets v1.4.0/go.mod h1:gpl+q95AzZlKVI3xSoseF9QPrypk0hQqBiJYeB/cR/I=
+github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/internal v1.2.0 h1:nCYfgcSyHZXJI8J0IWE5MsCGlb2xp9fJiXyxWgmOFg4=
+github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/internal v1.2.0/go.mod h1:ucUjca2JtSZboY8IoUqyQyuuXvwbMBVwFOm0vdQPNhA=
+github.com/AzureAD/microsoft-authentication-extensions-for-go/cache v0.1.1 h1:WJTmL004Abzc5wDB5VtZG2PJk5ndYDgVacGqfirKxjM=
+github.com/AzureAD/microsoft-authentication-extensions-for-go/cache v0.1.1/go.mod h1:tCcJZ0uHAmvjsVYzEFivsRTN00oz5BEsRgQHu5JZ9WE=
+github.com/AzureAD/microsoft-authentication-library-for-go v1.6.0 h1:XRzhVemXdgvJqCH0sFfrBUTnUJSBrBf7++ypk+twtRs=
+github.com/AzureAD/microsoft-authentication-library-for-go v1.6.0/go.mod h1:HKpQxkWaGLJ+D/5H8QRpyQXA1eKjxkFlOMwck5+33Jk=
+github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
+github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
+github.com/golang-jwt/jwt/v5 v5.3.0 h1:pv4AsKCKKZuqlgs5sUmn4x8UlGa0kEVt/puTpKx9vvo=
+github.com/golang-jwt/jwt/v5 v5.3.0/go.mod h1:fxCRLWMO43lRc8nhHWY6LGqRcf+1gQWArsqaEUEa5bE=
+github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
+github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
+github.com/keybase/go-keychain v0.0.1 h1:way+bWYa6lDppZoZcgMbYsvC7GxljxrskdNInRtuthU=
+github.com/keybase/go-keychain v0.0.1/go.mod h1:PdEILRW3i9D8JcdM+FmY6RwkHGnhHxXwkPPMeUgOK1k=
+github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc=
+github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw=
+github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c h1:+mdjkGKdHQG3305AYmdv1U2eRNDiU2ErMBj1gwrq8eQ=
+github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c/go.mod h1:7rwL4CYBLnjLxUqIJNnCWiEdr3bn6IUYi15bNlnbCCU=
+github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
+github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
+github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
+github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
+golang.org/x/crypto v0.47.0 h1:V6e3FRj+n4dbpw86FJ8Fv7XVOql7TEwpHapKoMJ/GO8=
+golang.org/x/crypto v0.47.0/go.mod h1:ff3Y9VzzKbwSSEzWqJsJVBnWmRwRSHt/6Op5n9bQc4A=
+golang.org/x/net v0.49.0 h1:eeHFmOGUTtaaPSGNmjBKpbng9MulQsJURQUAfUwY++o=
+golang.org/x/net v0.49.0/go.mod h1:/ysNB2EvaqvesRkuLAyjI1ycPZlQHM3q01F02UY/MV8=
+golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.40.0 h1:DBZZqJ2Rkml6QMQsZywtnjnnGvHza6BTfYFWY9kjEWQ=
+golang.org/x/sys v0.40.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
+golang.org/x/text v0.33.0 h1:B3njUFyqtHDUI5jMn1YIr5B0IE2U0qck04r6d4KPAxE=
+golang.org/x/text v0.33.0/go.mod h1:LuMebE6+rBincTi9+xWTY8TztLzKHc/9C1uBCG27+q8=
+gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
+gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
diff --git a/modules/azure/lowkeyvault/testdata/main.go b/modules/azure/lowkeyvault/testdata/main.go
new file mode 100644
index 0000000000..7cd36857b7
--- /dev/null
+++ b/modules/azure/lowkeyvault/testdata/main.go
@@ -0,0 +1,89 @@
+package main
+
+import (
+ "context"
+ "crypto/tls"
+ "fmt"
+ "log"
+ "net/http"
+ "os"
+
+ "github.com/Azure/azure-sdk-for-go/sdk/azcore/cloud"
+ "github.com/Azure/azure-sdk-for-go/sdk/azcore/policy"
+ "github.com/Azure/azure-sdk-for-go/sdk/azcore/tracing"
+ "github.com/Azure/azure-sdk-for-go/sdk/azidentity"
+ "github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/azsecrets"
+)
+
+func run() error {
+ ctx := context.Background()
+
+ connUrl := os.Getenv("CONNECTION_URL")
+ log.Printf("Using Lowkey Vault endpoint: %s", connUrl)
+ tokenUrl := os.Getenv("IDENTITY_ENDPOINT")
+ log.Printf("Using token URL: %s", tokenUrl)
+
+ customTransport := http.DefaultTransport.(*http.Transport).Clone()
+ customTransport.TLSClientConfig = &tls.Config{InsecureSkipVerify: true}
+ httpClient := http.Client{Transport: customTransport}
+
+ resp, err := httpClient.Get(tokenUrl + "?resource=" + connUrl)
+ if err != nil {
+ log.Fatalf("failed to get token from token URL %v", err)
+ return err
+ }
+ err = resp.Body.Close()
+ if err != nil {
+ log.Fatalf("failed to get token from token URL 2 %v", err)
+ return err
+ }
+
+ cred, err := azidentity.NewDefaultAzureCredential(nil) // Will use Managed Identity via the Assumed Identity container
+ if err != nil {
+ log.Fatalf("failed to create credential: %v", err)
+ return err
+ }
+ secretClient, err := azsecrets.NewClient(connUrl,
+ cred,
+ &azsecrets.ClientOptions{ClientOptions: struct {
+ APIVersion string
+ Cloud cloud.Configuration
+ InsecureAllowCredentialWithHTTP bool
+ Logging policy.LogOptions
+ Retry policy.RetryOptions
+ Telemetry policy.TelemetryOptions
+ TracingProvider tracing.Provider
+ Transport policy.Transporter
+ PerCallPolicies []policy.Policy
+ PerRetryPolicies []policy.Policy
+ }{Transport: &httpClient}, DisableChallengeResourceVerification: true})
+ if err != nil {
+ log.Fatalf("failed to create secret client: %v", err)
+ return err
+ }
+
+ secretName := "secret-name"
+ secretValue := "a secret value"
+ created, err := secretClient.SetSecret(ctx, secretName, azsecrets.SetSecretParameters{Value: &secretValue}, nil)
+ if err != nil {
+ log.Fatalf("failed to set the secret %v", err)
+ return err
+ }
+
+ fetched, err := secretClient.GetSecret(ctx, secretName, created.ID.Version(), nil)
+ if err != nil {
+ log.Fatalf("failed to get the secret %v", err)
+ return err
+ }
+ fetchedValue := *fetched.Secret.Value
+
+ fmt.Println(fetchedValue == secretValue)
+
+ return nil
+}
+
+func main() {
+ if err := run(); err != nil {
+ log.Fatal(err)
+ }
+}