feat: STACKIT Secrets Manager Action
Some checks failed
Release Secrets Manager Action / build (push) Has been cancelled
Some checks failed
Release Secrets Manager Action / build (push) Has been cancelled
This commit is contained in:
commit
cc0c27a4e9
9 changed files with 401 additions and 0 deletions
52
.forgejo/workflows/release.yml
Normal file
52
.forgejo/workflows/release.yml
Normal 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
51
README.md
Normal 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
59
action.yml
Normal 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
75
config/config.go
Normal 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
21
go.mod
Normal 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
38
go.sum
Normal 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
27
main.go
Normal 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
41
secretsmanager/client.go
Normal 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
37
secretsmanager/secrets.go
Normal 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
|
||||
}
|
||||
Loading…
Reference in a new issue