Blog
September 23, 2020 Marie H.

Managing Encryption Keys with IBM Key Protect and Go

Managing Encryption Keys with IBM Key Protect and Go

Photo by <a href="https://unsplash.com/@sasun1990?utm_source=cloudista&utm_medium=referral" target="_blank" rel="noopener">Sasun Bughdaryan</a> on <a href="https://unsplash.com/?utm_source=cloudista&utm_medium=referral" target="_blank" rel="noopener">Unsplash</a>

Managing Encryption Keys with IBM Key Protect and Go

I work on a Go gRPC service that sits between storage and the applications that need encrypted data. The service is responsible for key lifecycle — provisioning keys, wrapping data encryption keys before they're handed to storage, unwrapping them on the way out. Key Protect is the root key store underneath it. The service never sees plaintext data, only DEKs and their wrapped counterparts. That separation of concerns is the point.

Key Protect is IBM Cloud's managed key management service (KMS). If you're on AWS, think KMS. If you're on GCP, think Cloud KMS. Same concept, IBM-specific implementation.

The Go SDK is the interface I use most, and I want to walk through the real operations: creating keys, wrapping and unwrapping for envelope encryption, and the BYOK flow for protecting IBM Cloud services.

Updated March 2026: IBM Key Protect has continued to evolve as part of IBM Cloud's broader security portfolio. Some API details and SDK method signatures may have changed since this post. Always check the current IBM Key Protect Go SDK documentation. IBM also offers Hyper Protect Crypto Services (HPCS) for hardware-backed keys (FIPS 140-2 Level 4), which integrates with similar patterns.

Key Hierarchy: Root Keys and Standard Keys

Key Protect has two key types, and the distinction matters.

Root keys are symmetric wrapping keys that never leave Key Protect. You can't export a root key. You use root keys to wrap (encrypt) other keys — the "envelope encryption" pattern. Root keys support automatic rotation.

Standard keys are importable/exportable symmetric keys. You can use them for encrypting data directly, or you can bring your own key material (BYOK). Standard keys can leave the service.

For most workloads, you use root keys for envelope encryption: the root key wraps your data encryption key (DEK), and you store the wrapped DEK alongside your encrypted data. To decrypt, you unwrap the DEK using the root key, then decrypt your data. The DEK never leaves your application in plaintext for long, and the root key never leaves Key Protect.

Setting Up the Go Client

The IBM Key Protect Go SDK is available at github.com/IBM/keyprotect-go-client. Authentication uses IBM Cloud IAM — you'll need an API key.

import (
    "context"
    kp "github.com/IBM/keyprotect-go-client"
)

func newKeyProtectClient(apiKey, instanceID, region string) (*kp.Client, error) {
    cc := kp.ClientConfig{
        BaseURL:    kp.DefaultBaseURL,
        APIKey:     apiKey,
        InstanceID: instanceID,
        Region:     region,
        Verbose:    kp.VerboseFailOnly,
    }
    return kp.New(cc, kp.DefaultTransport())
}

The InstanceID is your Key Protect service instance ID from IBM Cloud — not to be confused with a key ID. You get it from the resource list or via ibmcloud resource service-instance <name> --id.

Creating a Root Key

func createRootKey(ctx context.Context, client *kp.Client, name string) (string, error) {
    key, err := client.CreateRootKey(ctx, name, nil)
    if err != nil {
        return "", fmt.Errorf("creating root key %q: %w", name, err)
    }
    return key.ID, nil
}

The nil second argument means let Key Protect generate the key material. If you're doing BYOK, you pass your own key material as a base64-encoded byte slice. The returned key.ID is what you store — it's the stable reference to the root key.

Wrapping and Unwrapping (Envelope Encryption)

This is the core operation for services that encrypt data. You generate a DEK locally, wrap it with a root key, store the wrapped DEK, and discard the plaintext DEK.

func wrapDEK(ctx context.Context, client *kp.Client, rootKeyID string, dek []byte) ([]byte, error) {
    // dek is your plaintext data encryption key (32 bytes for AES-256)
    wrappedDEK, err := client.Wrap(ctx, rootKeyID, dek, nil)
    if err != nil {
        return nil, fmt.Errorf("wrapping DEK with root key %s: %w", rootKeyID, err)
    }
    return wrappedDEK, nil
}

func unwrapDEK(ctx context.Context, client *kp.Client, rootKeyID string, wrappedDEK []byte) ([]byte, error) {
    plaintext, _, err := client.Unwrap(ctx, rootKeyID, wrappedDEK, nil)
    if err != nil {
        return nil, fmt.Errorf("unwrapping DEK with root key %s: %w", rootKeyID, err)
    }
    return plaintext, nil
}

The nil additional authentication data (AAD) parameter is optional — you can pass additional context bytes that must match between wrap and unwrap calls. I use this for binding the wrapped key to a specific resource ID, which prevents a wrapped DEK from being used to decrypt a different resource's data.

// Bind the wrapped key to a specific resource
aad := [][]string{{resourceID}}
wrappedDEK, err := client.Wrap(ctx, rootKeyID, dek, &aad)
// unwrap must use the same aad
plaintext, _, err := client.Unwrap(ctx, rootKeyID, wrappedDEK, &aad)

Listing and Rotating Keys

func listRootKeys(ctx context.Context, client *kp.Client) ([]kp.Key, error) {
    keys, err := client.GetKeys(ctx, 100, 0)
    if err != nil {
        return nil, fmt.Errorf("listing keys: %w", err)
    }
    return keys.Keys, nil
}

func rotateRootKey(ctx context.Context, client *kp.Client, keyID string) error {
    err := client.Rotate(ctx, keyID, "")
    if err != nil {
        return fmt.Errorf("rotating key %s: %w", keyID, err)
    }
    return nil
}

After rotating a root key, existing wrapped DEKs remain valid — Key Protect keeps previous key versions for unwrap operations. New wrap operations use the current version. This is the same model as AWS KMS key rotation. You don't need to re-wrap everything immediately when you rotate; the old wraps are still unwrappable.

BYOK for IBM Cloud Services

BYOK (bring your own key) lets you use a Key Protect root key to protect the encryption of IBM Cloud services like Cloud Object Storage, Db2, and others. The service uses your root key to wrap its own internal encryption keys. If you delete or disable your root key, the service can no longer decrypt its data. You own the kill switch.

Setting up BYOK is mostly done through IBM Cloud IAM — you grant the target service permission to use your Key Protect instance, then configure the service to use your key. At that point, every object or record the service encrypts is protected by a key hierarchy that ultimately traces back to your root key.

The Go SDK isn't directly involved in provisioning BYOK relationships — that's IAM and service configuration. But the SDK is how you manage the lifecycle: rotating the key on schedule, monitoring key state, and handling programmatic rotation in your own services.

Handling Errors

Key Protect API errors come back with HTTP status codes. The SDK surfaces them, but you need to handle a few cases explicitly:

if err != nil {
    var kpErr *kp.Error
    if errors.As(err, &kpErr) {
        switch kpErr.StatusCode {
        case 409:
            // Key already exists or conflict
        case 410:
            // Key has been deleted
        case 429:
            // Rate limited — back off and retry
        }
    }
    return err
}

Rate limiting (429) is the one I see most in bulk operations. Build in exponential backoff for any code that calls Key Protect in a loop.

Why Not Just Encrypt in the Application?

The question I get sometimes is: why use a KMS at all? Just encrypt with a key stored in a config file.

The answer is key lifecycle management. Rotating a key in a config file means re-deploying every service that uses it. Auditing which services have access to which keys is manual. Revoking a key requires coordinated deployment. A KMS gives you centralized access control, audit logs, automatic rotation, and a hardware-backed key store — all things that are genuinely difficult to build correctly yourself. The wrap/unwrap pattern also means your plaintext key material never lives in your database or object storage, even temporarily. That's a meaningful security property.

One pattern that's worked well for us: a dedicated key provider service that owns all KMS interactions, exposed over gRPC with mTLS. Other services don't call Key Protect directly — they call the key provider, which handles caching, retry logic, rate limiting, and audit logging centrally. If Key Protect's API changes or we need to swap KMS backends, we update one service. The consuming services just call WrapKey and UnwrapKey on a protobuf-defined interface and don't care what's underneath. The mTLS certificates ensure only authorized services can call it at all.