Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
31 changes: 29 additions & 2 deletions .github/workflows/main.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,11 @@ jobs:
build:
name: Build
runs-on: "ubuntu-latest"
permissions:
id-token: write
packages: write
contents: read
attestations: write
steps:

- name: Checkout
Expand All @@ -12,7 +17,7 @@ jobs:
- name: Setup Go
uses: actions/setup-go@v5
with:
go-version: '1.22.x'
go-version: '1.23.x'
check-latest: true

- name: Print Go Version
Expand Down Expand Up @@ -68,6 +73,11 @@ jobs:
make
fi

- uses: actions/attest-build-provenance@v2
if: ${{ env.PUBLISH == 'yes' }}
with:
subject-path: 'release/**/dockcmd*'

- name: Release Artifacts
if: ${{ env.RELEASE == 'yes' }}
run: |
Expand Down Expand Up @@ -103,7 +113,8 @@ jobs:

- name: Build and push
if: ${{ env.PUBLISH == 'yes' }}
uses: docker/build-push-action@v4
id: push
uses: docker/build-push-action@v6
with:
context: .
platforms: linux/amd64,linux/arm64
Expand All @@ -113,3 +124,19 @@ jobs:
tags: |
boxboat/dockcmd:${{ env.CI_VERSION }}
ghcr.io/boxboat/dockcmd:${{ env.CI_VERSION }}

- name: Attest ghcr image
if: ${{ env.PUBLISH == 'yes' }}
uses: actions/attest-build-provenance@v2
with:
subject-name: ghcr.io/boxboat/dockcmd:${{ env.CI_VERSION }}
subject-digest: ${{ steps.push.outputs.digest }}
push-to-registry: true

- name: Attest hub image
if: ${{ env.PUBLISH == 'yes' }}
uses: actions/attest-build-provenance@v2
with:
subject-name: boxboat/dockcmd:${{ env.CI_VERSION }}
subject-digest: ${{ steps.push.outputs.digest }}
push-to-registry: true
2 changes: 1 addition & 1 deletion Dockerfile
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
ARG VERSION=develop
ARG GO_VERSION=1.22.3
ARG GO_VERSION=1.23.4

FROM --platform=${BUILDPLATFORM} golang:${GO_VERSION}-alpine as build

Expand Down
22 changes: 11 additions & 11 deletions cmd/azure.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,12 +24,12 @@ import (
)

var (
client *azure.SecretsClient
clientID string
clientSecret string
tenantID string
useAzCliLogin bool
keyVaultName string
client *azure.SecretsClient
clientID string
clientSecret string
tenantID string
useChainedTokenCredentials bool
keyVaultName string
)

// azureRegionCmdPersistentPreRunE checks required persistent tokens for azureCmd
Expand All @@ -43,9 +43,9 @@ func azureCmdPersistentPreRunE(cmd *cobra.Command, args []string) error {
clientID = viper.GetString("client-id")
clientSecret = viper.GetString("client-secret")

if (tenantID == "" && clientID == "" && clientSecret == "") || useAzCliLogin {
if (clientID == "" && clientSecret == "") || useChainedTokenCredentials {
// set to true in case where no service principal credentials provided
useAzCliLogin = true
useChainedTokenCredentials = true
}

return nil
Expand Down Expand Up @@ -101,8 +101,8 @@ keyD: "<value-of-secret/root-from-azure-key-vault>"
azure.WithContext(azureCmd.Context()),
}

if useAzCliLogin {
opts = append(opts, azure.UseAzCliLogin())
if useChainedTokenCredentials {
opts = append(opts, azure.UseChainCredentials(), azure.TenantID(tenantID))
} else {
opts = append(opts, azure.ClientIDAndSecret(clientID, clientSecret), azure.TenantID(tenantID))
}
Expand Down Expand Up @@ -139,7 +139,7 @@ func init() {
// azure command and common persistent flags
azureCmd.AddCommand(azureGetSecretsCmd)
azureCmd.PersistentFlags().BoolVarP(
&useAzCliLogin, "az-cli-login",
&useChainedTokenCredentials, "az-cli-login",
"",
false,
"access credentials provided by az login - default if tenant, client-id and client-secret are not set")
Expand Down
102 changes: 73 additions & 29 deletions cmd/azure/azure.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,29 +17,59 @@ package azure
import (
"context"
"encoding/json"
"errors"
"fmt"
"strings"
"time"

"github.com/Azure/azure-sdk-for-go/profiles/2019-03-01/keyvault/keyvault"
"github.com/Azure/go-autorest/autorest/azure/auth"
"github.com/Azure/azure-sdk-for-go/sdk/azcore"
"github.com/Azure/azure-sdk-for-go/sdk/azcore/policy"
"github.com/Azure/azure-sdk-for-go/sdk/azidentity"
"github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/azsecrets"
"github.com/boxboat/dockcmd/cmd/common"
"github.com/patrickmn/go-cache"
)

const (
azurePublicKeyVault = "vault.azure.net"
keyVaultResource = "https://" + azurePublicKeyVault
azurePublicKeyVault = "vault.azure.net"
keyVaultResourceFormatString = "https://%s." + azurePublicKeyVault + "/"
)

type timeoutWrapper struct {
cred *azidentity.ManagedIdentityCredential
timeout time.Duration
}

// GetToken implements the azcore.TokenCredential interface
func (w *timeoutWrapper) GetToken(ctx context.Context, opts policy.TokenRequestOptions) (azcore.AccessToken, error) {
var tk azcore.AccessToken
var err error
if w.timeout > 0 {
c, cancel := context.WithTimeout(ctx, w.timeout)
defer cancel()
tk, err = w.cred.GetToken(c, opts)
if ce := c.Err(); errors.Is(ce, context.DeadlineExceeded) {
// The Context reached its deadline, probably because no managed identity is available.
// A credential unavailable error signals the chain to try its next credential, if any.
err = azidentity.NewCredentialUnavailableError("managed identity timed out")
} else {
// some managed identity implementation is available, so don't apply the timeout to future calls
w.timeout = 0
}
} else {
tk, err = w.cred.GetToken(ctx, opts)
}
return tk, err
}

type KeyVaultGetSecretAPI interface {
GetSecret(ctx context.Context, vaultBaseURL string, secretName string, secretVersion string) (result keyvault.SecretBundle, err error)
GetSecret(ctx context.Context, name string, version string, options *azsecrets.GetSecretOptions) (azsecrets.GetSecretResponse, error)
}

type SecretsClient struct {
common.SecretClient
keyVaultName string
keyVault keyvault.BaseClient
keyVault *azsecrets.Client
SecretCache *cache.Cache
ctx context.Context
api KeyVaultGetSecretAPI
Expand All @@ -50,14 +80,14 @@ type SecretsClientOpt interface {
}

type secretsClientOpts struct {
ctx context.Context
clientID string
clientSecret string
tenantID string
keyVaultName string
useAzCliLogin bool
cacheTTL time.Duration
api *KeyVaultGetSecretAPI
ctx context.Context
clientID string
clientSecret string
tenantID string
keyVaultName string
useChainCredentials bool
cacheTTL time.Duration
api *KeyVaultGetSecretAPI
}

type secretsClientOptFn func(opts *secretsClientOpts) error
Expand Down Expand Up @@ -91,9 +121,9 @@ func TenantID(tenantID string) SecretsClientOpt {
})
}

func UseAzCliLogin() SecretsClientOpt {
func UseChainCredentials() SecretsClientOpt {
return secretsClientOptFn(func(opts *secretsClientOpts) error {
opts.useAzCliLogin = true
opts.useChainCredentials = true
return nil
})
}
Expand Down Expand Up @@ -136,22 +166,36 @@ func NewSecretsClient(opts ...SecretsClientOpt) (*SecretsClient, error) {
ctx: o.ctx,
}
if o.api == nil {
if o.useAzCliLogin {
client.keyVault = keyvault.New()
authorizer, err := auth.NewAuthorizerFromCLIWithResource(keyVaultResource)
if o.useChainCredentials {
common.Logger.Debugf("using chain credentials")

managed, err := azidentity.NewManagedIdentityCredential(nil)
if err != nil {
return nil, err
}
client.keyVault.Authorizer = authorizer

azCli, err := azidentity.NewAzureCLICredential(&azidentity.AzureCLICredentialOptions{AdditionallyAllowedTenants: []string{o.tenantID}})
if err != nil {
return nil, err
}

chain, err := azidentity.NewChainedTokenCredential([]azcore.TokenCredential{&timeoutWrapper{managed, time.Second}, azCli}, nil)
if err != nil {
return nil, err
}

azSecretsClient, err := azsecrets.NewClient(fmt.Sprintf(keyVaultResourceFormatString, client.keyVaultName), chain, nil)
client.keyVault = azSecretsClient

} else {
client.keyVault = keyvault.New()
clientConfig := auth.NewClientCredentialsConfig(o.clientID, o.clientSecret, o.tenantID)
clientConfig.Resource = keyVaultResource
authorizer, err := clientConfig.Authorizer()

cred, err := azidentity.NewClientSecretCredential(o.tenantID, o.clientID, o.clientSecret, nil)
if err != nil {
return nil, err
}
client.keyVault.Authorizer = authorizer
azSecretsClient, err := azsecrets.NewClient(fmt.Sprintf(keyVaultResourceFormatString, client.keyVaultName), cred, nil)
client.keyVault = azSecretsClient

}
client.api = client.keyVault
} else {
Expand Down Expand Up @@ -185,9 +229,9 @@ func (c *SecretsClient) GetJSONSecret(secretName string, secretKey string) (stri

secretResp, err := c.api.GetSecret(
c.ctx,
"https://"+c.keyVaultName+".vault.azure.net",
adjustedSecretName,
version)
version,
nil)
if err != nil {
return "", err
}
Expand Down Expand Up @@ -234,9 +278,9 @@ func (c *SecretsClient) GetTextSecret(secretName string) (string, error) {

secretResp, err := c.api.GetSecret(
c.ctx,
"https://"+c.keyVaultName+"."+azurePublicKeyVault,
adjustedSecretName,
version)
version,
nil)
if err != nil {
return "", err
}
Expand Down
32 changes: 18 additions & 14 deletions cmd/azure/azure_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,14 +21,14 @@ import (
"testing"
"text/template"

"github.com/Azure/azure-sdk-for-go/profiles/2019-03-01/keyvault/keyvault"
"github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/azsecrets"
"github.com/boxboat/dockcmd/cmd/common"
)

type mockGetSecretValueAPI func(ctx context.Context, vaultBaseURL string, secretName string, secretVersion string) (result keyvault.SecretBundle, err error)
type mockGetSecretValueAPI func(ctx context.Context, name string, version string, options *azsecrets.GetSecretOptions) (azsecrets.GetSecretResponse, error)

func (m mockGetSecretValueAPI) GetSecret(ctx context.Context, vaultBaseURL string, secretName string, secretVersion string) (result keyvault.SecretBundle, err error) {
return m(ctx, vaultBaseURL, secretName, secretVersion)
func (m mockGetSecretValueAPI) GetSecret(ctx context.Context, name string, version string, options *azsecrets.GetSecretOptions) (azsecrets.GetSecretResponse, error) {
return m(ctx, name, version, nil)
}

func TestSecretsClient_GetTextSecret(t *testing.T) {
Expand All @@ -40,17 +40,19 @@ func TestSecretsClient_GetTextSecret(t *testing.T) {
}{
{
client: func(t *testing.T) KeyVaultGetSecretAPI {
return mockGetSecretValueAPI(func(ctx context.Context, vaultBaseURL string, secretName string, secretVersion string) (result keyvault.SecretBundle, err error) {
return mockGetSecretValueAPI(func(ctx context.Context, name string, version string, options *azsecrets.GetSecretOptions) (azsecrets.GetSecretResponse, error) {
t.Helper()
if secretName == "" {
if name == "" {
t.Fatalf("expect secretName to not be empty")
}
secretString := ""
if secretName == "alpha" {
if name == "alpha" {
secretString = "charlie"
}
return keyvault.SecretBundle{
Value: &secretString,
return azsecrets.GetSecretResponse{
Secret: azsecrets.Secret{
Value: &secretString,
},
}, nil
})
},
Expand Down Expand Up @@ -93,17 +95,19 @@ func TestSecretsClient_GetJSONSecret(t *testing.T) {
}{
{
client: func(t *testing.T) KeyVaultGetSecretAPI {
return mockGetSecretValueAPI(func(ctx context.Context, vaultBaseURL string, secretName string, secretVersion string) (result keyvault.SecretBundle, err error) {
return mockGetSecretValueAPI(func(ctx context.Context, name string, version string, options *azsecrets.GetSecretOptions) (azsecrets.GetSecretResponse, error) {
t.Helper()
if secretName == "" {
if name == "" {
t.Fatalf("expect secretName to not be empty")
}
secretString := ""
if secretName == "alpha" {
if name == "alpha" {
secretString = `{"bravo":"foo", "charlie":"bar"}`
}
return keyvault.SecretBundle{
Value: &secretString,
return azsecrets.GetSecretResponse{
Secret: azsecrets.Secret{
Value: &secretString,
},
}, nil
})
},
Expand Down
Loading