1Password and Go Configuration

injecting 1Password values securely at runtime

  ·  4 min read

Application Configuration #

As with any application, we often need to configure a combination of settings. Some may be public and fixed, while others are sensitive and must be stored securely. Most will need different values across environments.

A common approach is to supply environment variables to the application via external processes—such as a .env file, shell scripts, or sed replacements.

For remote deployments, the process might look a bit different. You might have a CI/CD pipeline that sets environment variables, or a secrets‑management solution that injects them into your deployment via a volume mount or other means.

I’ve used a combination of both approaches over the years. They work well and follow the Twelve‑Factor App guidance. Great!

For the sake of velocity, I set out to store configuration consistently across environments while still managing it securely. It was important to have the same experience in local development and remote deployment.

Enter 1Password #

1Password is a password manager I’ve used for quite some time. They provide a great developer experience across the CLI, API, and SDKs.

They offer authorization via browseruser consent, service accounts, and the Connect server access token. I haven’t used the Connect server access token, but I’ve used the op CLI and service accounts extensively.

With the CLI, you don’t need additional secrets in your environment to authenticate with 1Password. However, it’s harder to use in remote deployments because it requires the user to authorize access to the 1Password account, which it might be cumbersome to do in a remote environment.

Enter op-viper #

op-viper is a tool I created to bridge 1Password and Go configuration using viper. It lets me store secrets in 1Password and inject them into my application at runtime via 1Password references.

Installing op-viper #

go get github.com/rafaelbroseghini/op-viper

How it works #

Let’s say I have a configuration file like this:

database:
  host: "localhost"
  port: 5432
  username: "{{ op://vault/item/username }}"
  password: "{{ op://vault/item/password }}"

api:
  key: "{{ op://production/api-key }}"
  secret: "{{ op://production/api-secret }}"

Using op-viper with the CLI #

Let’s say I have the following Go code:

package main

import (
    "context"
    "github.com/spf13/viper"
    "github.com/rafaelbroseghini/op-viper/pkg/onepassword"
)

type Config struct {
    DatabaseURL string `mapstructure:"database_url"`
    APIKey      string `mapstructure:"api_key"`
}

func main() {
    v := viper.New()
    v.SetConfigType("yaml")
    v.SetConfigName("config.yaml")
    v.AddConfigPath(".")
    
    v.ReadInConfig()
    
    var config Config
    l := onepassword.NewDefaultLoader()
    v.Unmarshal(&config, viper.DecodeHook(l.OnePasswordHookFunc(context.Background())))
}

As part of unmarshaling, op-viper replaces 1Password references with actual values and binds them to the Config struct. NewDefaultLoader() executes op shell commands to fetch values from 1Password. If the token has expired or is not present, it prompts the user to authorize access to their 1Password account.

Using op-viper with service accounts #

package main

import (
    "context"
    "github.com/spf13/viper"
    "github.com/rafaelbroseghini/op-viper/pkg/onepassword"
)

type Config struct {
    DatabaseURL string `mapstructure:"database_url"`
    APIKey      string `mapstructure:"api_key"`
}

func main() {
    v := viper.New()
    v.SetConfigType("yaml")
    v.SetConfigName("config.yaml")
    v.AddConfigPath(".")
    
    v.ReadInConfig()
    
    var config Config
    ctx := context.Background()
    sdkClient := onepassword.NewOnePasswordSDKClient(
        ctx,
        "my-app",           // integration name
        "1.0.0",           // version
        "your-service-account-token",
    )

    loader := onepassword.NewLoader(
        onepassword.WithSDKClient(sdkClient),
        onepassword.WithPrefix("${"),  // Custom prefix
        onepassword.WithSuffix("}"),   // Custom suffix
    )

    v.Unmarshal(&config, viper.DecodeHook(loader.OnePasswordHookFunc(ctx)))

With this approach, op-viper uses the onepassword-sdk-go client to fetch values from 1Password and bind them to the Config struct. This requires a service account token to be available (via OP_SERVICE_ACCOUNT_TOKEN environment variable) before running the application.

In a real application, you may want to detect the runtime environment and choose between the CLI or SDK client loader.

...
var loader onepassword.Loader
if isLocalEnvironment() {
    loader = onepassword.NewDefaultLoader()
} else {
    loader = onepassword.NewLoader(
        onepassword.WithSDKClient(sdkClient),
    )
}

Conclusion #

op-viper lets me ditch .env files, shell scripts, and other approaches for injecting configuration, and use a consistent approach to managing configuration across environments.

To be transparent, changes to 1Password references in your config files require redeploying the application, which might not be ideal for some use cases. If you work solo or in a small team where configuration doesn’t change frequently, this might be a good fit for you 🙂

Check out the repository for more details!