feat: STACKIT Secrets Manager Action
Some checks failed
Release Secrets Manager Action / build (push) Has been cancelled

This commit is contained in:
Timo Bergen 2025-07-07 21:32:39 +02:00
commit cc0c27a4e9
9 changed files with 401 additions and 0 deletions

View file

@ -0,0 +1,52 @@
name: Release Secrets Manager Action
on:
push:
tags:
- v*
env:
build_name: action-secretsmanager
jobs:
build:
runs-on: docker
steps:
- name: 📤 Checkout source code
uses: actions/checkout@v4
- name: ⚙️ Set up Go
uses: actions/setup-go@v5
with:
go-version: 1.24
- name: ⚙️ Install dependencies
run: |
apt-get -y -qq update && apt-get -qq -y install jq
- name: 👨🏻‍🔧 Build app
run: |
go build -o ${{ env.build_name }}
- name: 🤠 Create release
run: |
set -e
echo "Creating release for ${{ env.GITHUB_REPOSITORY}} with tag ${{ env.GITHUB_REF_NAME }}"
REQUEST=$(curl --request POST \
--url ${{ env.GITHUB_API_URL }}/repos/${{ env.GITHUB_REPOSITORY }}/releases \
--header 'Authorization: token ${{ secrets.GIT_TOKEN }}' \
--header 'content-type: application/json' \
--data '{ "tag_name": "${{ env.GITHUB_REF_NAME }}" }')
ls -lh ${{ env.build_name }}
RELEASE_ID=$(echo $REQUEST | jq .id)
echo "Uploading release asset for Release ID ${RELEASE_ID}"
curl --request POST \
--url ${{ env.GITHUB_API_URL }}/repos/${{ env.GITHUB_REPOSITORY }}/releases/${RELEASE_ID}/assets?name=${{ env.build_name }} \
--header 'Authorization: token ${{ secrets.GIT_TOKEN }}' \
-F 'attachment=@${{ env.build_name}}'

51
README.md Normal file
View file

@ -0,0 +1,51 @@
# STACKIT Secrets Manager Action
## parameters
| parameter | description | default |
| --- | --- | --- |
| vault_addr | Secrets Manager Base URL | https://prod.sm.eu01.stackit.cloud |
| vault_id | Your Secrets Manager ID, looks something like this: 6d9060fd-59b4-4dda-9106-b2dbe88acf65 | - |
| vault_username | Your Secrets Manager Username, looks something like this: sms96o170771ttt6 | - |
| vault_password | Your Secrets Manager Password, a random generated password provided by the STACKIT Portal | - |
| vault_path | The Path to your Secret can be some like this: "test" or "folder/test" | - |
| debug | true or false, enable or disable Debug Logging | false |
## usage
In this example we assume that there is a Secret on Path "${{ secrets.VAULT_PATH}}" and there is a KVSecret named "test".
In the "Output secret" step we output above mentioned KVSecret "test". We access the outputs of the secrets step.
Keep in mind to set an id on the actions step and use that to reference the outputted secrets.
```yaml
name: Secrets Manager Action
on:
push:
workflow_dispatch:
jobs:
get-vault-secrets:
runs-on: docker
# here we can also define outputs for use in other stages
# keep in mind that other "stages" need to define a "needs" for this job
outputs:
# here i use our example secret "test"
test: ${{ steps.fetch-secrets.outputs.test }}
steps:
- name: Fetch secrets from STACKIT Secrets Manager
id: secrets
uses: https://stackit-solutions.git.onstackit.cloud/actions/secretsmanager@main
with:
# vault_addr: 'https://prod.sm.eu01.stackit.cloud' # Optional - uses default STACKIT endpoint
vault_id: ${{ secrets.VAULT_ID }} # Your Secrets Manager ID
vault_username: ${{ secrets.VAULT_USERNAME }} # Your STACKIT Secrets Manager username
vault_password: ${{ secrets.VAULT_PASSWORD }} # Your STACKIT Secrets Manager password
vault_path: ${{ secrets.VAULT_PATH }} # The secret key/path in your Secrets Manager
debug: false # Set to 'true' for debug logging
- name: Output secret
run: |
echo ${{ steps.secrets.outputs.test}}
```

59
action.yml Normal file
View file

@ -0,0 +1,59 @@
name: STACKIT Secrets Manager Secret Fetcher
description: Connects to Secrets Manager using a Go app, gets all secrets under a path, and outputs them.
inputs:
go_version:
description: The version of Go to use for building the application.
required: false
default: 1.24.x
vault_addr:
description: You could optionally override the address.
required: false
vault_id:
description: The ID of your Secrets Manager Instance.
required: true
vault_username:
description: The Vault username to use for authentication.
required: true
vault_password:
description: The Vault password to use for authentication.
vault_path:
description: The path in Vault where the secrets are stored (e.g., secret/data/my-app).
required: true
debug:
description: Turn on debugging logs.
required: false
default: false
outputs:
secrets:
description: A JSON object string containing all the fetched secrets.
runs:
using: composite
steps:
- name: Set up Go
uses: actions/setup-go@v5
with:
go-version: ${{ inputs.go_version }}
- name: Check out action code
uses: actions/checkout@v4
with:
repository: actions/secretsmanager
ref: main
github-server-url: https://stackit-solutions.git.onstackit.cloud
- name: Run Vault Fetcher and set output
id: secrets
run: |
go mod tidy
go run main.go >> $GITHUB_OUTPUT
shell: bash
env:
VAULT_ADDR: ${{ inputs.vault_addr }}
VAULT_ID: ${{ inputs.vault_id }}
VAULT_USERNAME: ${{ inputs.vault_username }}
VAULT_PASSWORD: ${{ inputs.vault_password }}
VAULT_PATH: ${{ inputs.vault_path }}
DEBUG: ${{ inputs.debug }}

75
config/config.go Normal file
View file

@ -0,0 +1,75 @@
package config
import (
"log"
"os"
"github.com/creasty/defaults"
)
type Config struct {
VaultAddr string `default:"https://prod.sm.eu01.stackit.cloud" env:"VAULT_ADDR"`
VaultUsername string `env:"VAULT_USERNAME"`
VaultPassword string `env:"VAULT_PASSWORD"`
VaultSecretsManagerID string `env:"VAULT_ID"`
VaultPath string `env:"VAULT_PATH"`
Debug bool `default:"false" env:"DEBUG"`
}
// DebugLog prints debug messages only if DEBUG is enabled
func DebugLog(format string, args ...interface{}) {
if os.Getenv("DEBUG") == "true" {
log.Printf("DEBUG: "+format, args...)
}
}
// InfoLog prints info messages only if DEBUG is enabled
func InfoLog(format string, args ...interface{}) {
if os.Getenv("DEBUG") == "true" {
log.Printf("INFO: "+format, args...)
}
}
// ErrorLog prints error messages only if DEBUG is enabled
func ErrorLog(format string, args ...interface{}) {
if os.Getenv("DEBUG") == "true" {
log.Printf("ERROR: "+format, args...)
}
}
// FatalLog always prints fatal messages and exits
func FatalLog(format string, args ...interface{}) {
log.Fatalf("FATAL: "+format, args...)
}
func ValidateConfig(
cfg Config,
) Config {
defaults.Set(&cfg)
if cfg.VaultAddr == "" {
FatalLog("VAULT_ADDR cannot be empty")
}
if cfg.VaultUsername == "" {
FatalLog("VAULT_USERNAME cannot be empty")
}
if cfg.VaultPassword == "" {
FatalLog("VAULT_PASSWORD cannot be empty")
}
if cfg.VaultSecretsManagerID == "" {
FatalLog("VAULT_ID cannot be empty, in the Secrets Manager UI this is called 'Secrets Manager-ID'")
}
if cfg.VaultPath == "" {
FatalLog("VAULT_SECRET cannot be empty, this is the key of your secret")
}
InfoLog("Using Vault address: %s", cfg.VaultAddr)
InfoLog("Vault path: %s", cfg.VaultPath)
InfoLog("Mount: %s", cfg.VaultSecretsManagerID)
return cfg
}

21
go.mod Normal file
View file

@ -0,0 +1,21 @@
module secretsmanager
go 1.24.3
require (
github.com/caarlos0/env/v11 v11.3.1
github.com/creasty/defaults v1.8.0
github.com/hashicorp/vault-client-go v0.4.3
)
require (
github.com/hashicorp/go-cleanhttp v0.5.2 // indirect
github.com/hashicorp/go-retryablehttp v0.7.7 // indirect
github.com/hashicorp/go-rootcerts v1.0.2 // indirect
github.com/hashicorp/go-secure-stdlib/strutil v0.1.2 // indirect
github.com/mitchellh/go-homedir v1.1.0 // indirect
github.com/ryanuber/go-glob v1.0.0 // indirect
github.com/stretchr/testify v1.10.0 // indirect
golang.org/x/sys v0.31.0 // indirect
golang.org/x/time v0.0.0-20220922220347-f3bd1da661af // indirect
)

38
go.sum Normal file
View file

@ -0,0 +1,38 @@
github.com/caarlos0/env/v11 v11.3.1 h1:cArPWC15hWmEt+gWk7YBi7lEXTXCvpaSdCiZE2X5mCA=
github.com/caarlos0/env/v11 v11.3.1/go.mod h1:qupehSf/Y0TUTsxKywqRt/vJjN5nz6vauiYEUUr8P4U=
github.com/creasty/defaults v1.8.0 h1:z27FJxCAa0JKt3utc0sCImAEb+spPucmKoOdLHvHYKk=
github.com/creasty/defaults v1.8.0/go.mod h1:iGzKe6pbEHnpMPtfDXZEr0NVxWnPTjb1bbDy08fPzYM=
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/fatih/color v1.16.0 h1:zmkK9Ngbjj+K0yRhTVONQh1p/HknKYSlNT+vZCzyokM=
github.com/fatih/color v1.16.0/go.mod h1:fL2Sau1YI5c0pdGEVCbKQbLXB6edEj1ZgiY4NijnWvE=
github.com/hashicorp/go-cleanhttp v0.5.2 h1:035FKYIWjmULyFRBKPs8TBQoi0x6d9G4xc9neXJWAZQ=
github.com/hashicorp/go-cleanhttp v0.5.2/go.mod h1:kO/YDlP8L1346E6Sodw+PrpBSV4/SoxCXGY6BqNFT48=
github.com/hashicorp/go-hclog v1.6.3 h1:Qr2kF+eVWjTiYmU7Y31tYlP1h0q/X3Nl3tPGdaB11/k=
github.com/hashicorp/go-hclog v1.6.3/go.mod h1:W4Qnvbt70Wk/zYJryRzDRU/4r0kIg0PVHBcfoyhpF5M=
github.com/hashicorp/go-retryablehttp v0.7.7 h1:C8hUCYzor8PIfXHa4UrZkU4VvK8o9ISHxT2Q8+VepXU=
github.com/hashicorp/go-retryablehttp v0.7.7/go.mod h1:pkQpWZeYWskR+D1tR2O5OcBFOxfA7DoAO6xtkuQnHTk=
github.com/hashicorp/go-rootcerts v1.0.2 h1:jzhAVGtqPKbwpyCPELlgNWhE1znq+qwJtW5Oi2viEzc=
github.com/hashicorp/go-rootcerts v1.0.2/go.mod h1:pqUvnprVnM5bf7AOirdbb01K4ccR319Vf4pU3K5EGc8=
github.com/hashicorp/go-secure-stdlib/strutil v0.1.2 h1:kes8mmyCpxJsI7FTwtzRqEy9CdjCtrXrXGuOpxEA7Ts=
github.com/hashicorp/go-secure-stdlib/strutil v0.1.2/go.mod h1:Gou2R9+il93BqX25LAKCLuM+y9U2T4hlwvT1yprcna4=
github.com/hashicorp/vault-client-go v0.4.3 h1:zG7STGVgn/VK6rnZc0k8PGbfv2x/sJExRKHSUg3ljWc=
github.com/hashicorp/vault-client-go v0.4.3/go.mod h1:4tDw7Uhq5XOxS1fO+oMtotHL7j4sB9cp0T7U6m4FzDY=
github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=
github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG+4E0Y=
github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=
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/ryanuber/go-glob v1.0.0 h1:iQh3xXAumdQ+4Ufa5b25cRpC5TYKlno6hsv6Cb3pkBk=
github.com/ryanuber/go-glob v1.0.0/go.mod h1:807d1WSdnB0XRJzKNil9Om6lcp/3a0v4qIHxIXzX/Yc=
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
golang.org/x/sys v0.31.0 h1:ioabZlmFYtWhL+TRYpcnNlLwhyxaM9kWTDEmfnprqik=
golang.org/x/sys v0.31.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
golang.org/x/time v0.0.0-20220922220347-f3bd1da661af h1:Yx9k8YCG3dvF87UAn2tu2HQLf2dt/eR1bXxpLMWeH+Y=
golang.org/x/time v0.0.0-20220922220347-f3bd1da661af/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=

27
main.go Normal file
View file

@ -0,0 +1,27 @@
package main
import (
"log"
"fmt"
"secretsmanager/config"
"secretsmanager/secretsmanager"
"github.com/caarlos0/env/v11"
)
func main() {
var cfg config.Config
err := env.Parse(&cfg)
if err != nil {
log.Fatalf("Error parsing environment variables %s", err)
}
cfg = config.ValidateConfig(cfg)
s := secretsmanager.InitializeClient(cfg)
data, _ := secretsmanager.GetSecrets(&s, cfg)
for _, secret := range data {
fmt.Println(secret)
}
}

41
secretsmanager/client.go Normal file
View file

@ -0,0 +1,41 @@
package secretsmanager
import (
"context"
"time"
"secretsmanager/config"
"github.com/hashicorp/vault-client-go"
"github.com/hashicorp/vault-client-go/schema"
)
type SecretsManager struct {
Ctx context.Context
Client *vault.Client
}
func InitializeClient(
cfg config.Config,
) SecretsManager {
s := SecretsManager{}
s.Ctx = context.Background()
s.Client, _ = vault.New(
vault.WithAddress(cfg.VaultAddr),
vault.WithRequestTimeout(30*time.Second),
vault.WithTLS(vault.TLSConfiguration{
InsecureSkipVerify: false,
}),
)
config.InfoLog("Attempting to login with user %s", cfg.VaultUsername)
loginResp, err := s.Client.Auth.UserpassLogin(s.Ctx, cfg.VaultUsername, schema.UserpassLoginRequest{Password: cfg.VaultPassword})
if err != nil {
config.FatalLog("Vault login request failed: %s", err)
}
config.InfoLog("Login successful. Token received.")
s.Client.SetToken(loginResp.Auth.ClientToken)
return s
}

37
secretsmanager/secrets.go Normal file
View file

@ -0,0 +1,37 @@
package secretsmanager
import (
"fmt"
"secretsmanager/config"
"log"
"github.com/hashicorp/vault-client-go"
)
func GetSecrets(
s *SecretsManager,
cfg config.Config,
) ([]string, error) {
config.InfoLog("Attempting to read secret from mount '%s' at path '%s'", cfg.VaultSecretsManagerID, cfg.VaultPath)
secret, err := s.Client.Secrets.KvV2Read(s.Ctx, cfg.VaultPath, vault.WithMountPath(cfg.VaultSecretsManagerID))
if err != nil {
log.Fatalf("Failed to read secret from vault: %v", err)
return nil, fmt.Errorf("failed to read secret from vault: %w", err)
}
if secret == nil || secret.Data.Data == nil {
log.Fatal("No data found at the specified secret path.")
return []string{}, nil
}
var secretsAsKeyValue []string
for key, value := range secret.Data.Data {
secretsAsKeyValue = append(secretsAsKeyValue, fmt.Sprintf("%s=%v", key, value))
}
config.InfoLog("Successfully retrieved and formatted %d secrets.", len(secretsAsKeyValue))
return secretsAsKeyValue, nil
}