Clients & Libraries
Go
A context-aware client with typed services for secrets, certificates, SSH, GPG and OpenSSL keys, tokens, JKS keystores and operation-only crypto, plus Call() for any REST route. Requires Go 1.22 or newer.
Install
The module path is github.com/afterdarksys/secretserver-clients/go. Its source lives in the go/ directory of the clients repository. Add it to your module with:
go get github.com/afterdarksys/secretserver-clients/go@latest
Import the package as:
import ss "github.com/afterdarksys/secretserver-clients/go/secretserver"
Authenticate
The Go client does not read environment variables. Pass the key (and optionally the URL) in Config. APIURL defaults to https://api.secretserver.io. The default HTTP client has a 30 second timeout, TLS 1.2 minimum and never follows redirects.
client, err := ss.NewClient(&ss.Config{
APIKey: os.Getenv("SS_API_KEY"),
APIURL: "https://api.secretserver.io", // optional
})
if err != nil {
log.Fatal(err) // missing key or non-https URL
}Read a secret
By name
secret, err := client.Secrets.Get(ctx, "db-password", nil)
if err != nil {
return err
}
value := secret.Data["value"]
etag := secret.ETag // use as IfMatch on the next updateSecrets.Get always returns the current version. For a container path or an older version, call the path route directly:
By container path and version
var out struct {
Meta map[string]any `json:"meta"`
Data map[string]string `json:"data"`
}
path := "/s/" + url.PathEscape("production") + "/" + url.PathEscape("db-password") + "/2"
if _, err := client.Call(ctx, "GET", path, nil, &out); err != nil {
return err
}
previous := out.Data["value"]Write a secret
Create
container := "3f0c...uuid"
_, err := client.Secrets.Create(ctx, &ss.SecretCreateRequest{
Name: "db-password",
Data: map[string]string{"value": "s3cr3t"},
Description: "Primary database",
ContainerID: &container, // optional
Tags: []string{"prod"},
})Update
A nil field is left out and keeps the stored value. To clear description, tags or container_id, name it in Clear. Update returns ss.ErrPartialUpdatesUnconfirmed without sending anything unless Config.PartialUpdates is true or IfMatch holds an ETag from Get or Update.
cur, err := client.Secrets.Get(ctx, "db-password", nil)
if err != nil {
return err
}
_, err = client.Secrets.Update(ctx, "db-password", &ss.SecretUpdateRequest{
Data: map[string]string{"value": "new-value"},
IfMatch: cur.ETag,
})
// metadata only
_, err = client.Secrets.Update(ctx, "db-password", &ss.SecretUpdateRequest{
Clear: []string{"description"},
IfMatch: cur.ETag,
})Delete
err := client.Secrets.Delete(ctx, "db-password")
List secrets
secrets, err := client.Secrets.List(ctx, &ss.SecretListOptions{
Limit: 100, // 1..1000; 0 uses the server default (100)
Tags: []string{"prod"}, // filtered client-side
})
for _, s := range secrets {
fmt.Println(s.Name, s.Version)
}Error handling
| Error | Meaning |
|---|---|
*ss.ErrorResponse | Any non-2xx response. Response.StatusCode holds the status; the message never includes the response body. |
*ss.ConflictError | HTTP 409. ETag is the current value; errors.As also matches *ss.ErrorResponse. |
ss.ErrPartialUpdatesUnconfirmed | Update refused locally: no opt-in and no ETag. |
ss.ErrResponseTooLarge | Response exceeded 4 MiB (JSON) or 16 MiB (downloads). |
_, err := client.Secrets.Get(ctx, "db-password", nil)
var conflict *ss.ConflictError
var apiErr *ss.ErrorResponse
switch {
case err == nil:
case errors.As(err, &conflict):
log.Printf("stale update, current ETag %s", conflict.ETag)
case errors.As(err, &apiErr) && apiErr.Response.StatusCode == http.StatusNotFound:
log.Print("not found")
case errors.As(err, &apiErr) && apiErr.Response.StatusCode == http.StatusForbidden:
log.Print("API key lacks the required scope")
default:
log.Printf("request failed: %v", err) // transport error or context cancellation
}Complete example
main.go
package main
import (
"context"
"errors"
"fmt"
"log"
"net/http"
"os"
"time"
ss "github.com/afterdarksys/secretserver-clients/go/secretserver"
)
func main() {
client, err := ss.NewClient(&ss.Config{APIKey: os.Getenv("SS_API_KEY")})
if err != nil {
log.Fatal(err)
}
ctx, cancel := context.WithTimeout(context.Background(), 20*time.Second)
defer cancel()
const name = "example-api-token"
cur, err := client.Secrets.Get(ctx, name, nil)
var apiErr *ss.ErrorResponse
if errors.As(err, &apiErr) && apiErr.Response.StatusCode == http.StatusNotFound {
if _, err = client.Secrets.Create(ctx, &ss.SecretCreateRequest{
Name: name,
Data: map[string]string{"value": "first-value"},
}); err != nil {
log.Fatal(err)
}
cur, err = client.Secrets.Get(ctx, name, nil)
}
if err != nil {
log.Fatal(err)
}
if _, err := client.Secrets.Update(ctx, name, &ss.SecretUpdateRequest{
Data: map[string]string{"value": "second-value"},
IfMatch: cur.ETag,
}); err != nil {
log.Fatal(err)
}
all, err := client.Secrets.List(ctx, nil)
if err != nil {
log.Fatal(err)
}
fmt.Printf("%d secrets visible to this key\n", len(all))
}More operations
- Any REST route:
client.Call(ctx, method, path, body, &out), withpathrelative to/api/v1or including it. - Named variables:
AssignVariable,Render(ctx, text),ResolveDocument(ctx, json.RawMessage). - Services on the client:
Certificates,SSHKeys,GPGKeys,OpenSSLKeys,Passwords,Tokens,NTLMHashes,JKS,Crypto,Integrations,Transform,Intelligence,Extraction,LDAP. - Private CA: pass
Config.HTTPClientwith an*http.TransportwhoseTLSClientConfig.RootCAsincludes it. Transports that skip verification are refused.