<a id="how-use-secrets"></a>

# How to use secrets in an SDK

<!-- @tests in tests/docs-how-to/use-secrets/task.yaml -->
<!-- @artefact secret interface -->
<!-- @artefact workshopctl get-secret -->

An SDK that needs a credential at runtime,
such as an API key,
declares a `secret` plug.
After a user connects the plug to a secret slot,
the SDK can request the value when it is needed
without shipping or storing the value itself.

Use this guide to add one of the supported delivery patterns
to an existing SDK.

## Prerequisites

Before starting, ensure you have these requirements satisfied:

- An existing **SDKcraft** project.
- A command, tool, or systemd service in the SDK
  that needs a credential at runtime.
- A host keyring and the **secret-tool** utility
  to store a test value,
  as listed in [How to provide secrets to a workshop](https://ubuntu.com/workshop/docs/how-to/develop-with-workshops/provide-secrets.md#how-provide-secrets).

## Choose how the SDK receives the value

Choose the delivery mechanism based on what consumes the credential:

| What needs it                                   | Mechanism                                                   | Why                                                                                           |
|-------------------------------------------------|-------------------------------------------------------------|-----------------------------------------------------------------------------------------------|
| A tool with a credential-helper setting         | **workshopctl get-secret**,<br/>called directly by the tool | The tool requests the value only when needed<br/>without putting it in the tool’s environment |
| A command that reads standard input             | Pipe from **workshopctl get-secret**                        | The value can stay out of the command’s environment                                           |
| A command that requires an environment variable | An SDK-supplied wrapper                                     | The variable exists only in the wrapper and the process it starts                             |
| A long-running systemd service                  | `LoadCredential=` in the service unit                       | systemd requests the value on each start<br/>and makes it available as a credential file      |
| A value the user passes for a single command    | The user’s `--env` argument                                 | The user supplies the value ad hoc,<br/>without a slot or a connection                        |

#### WARNING
Never put a secret in SDK content, `sdkcraft.yaml`,
workshop definitions, hooks, shell profiles, examples, or logs.
Each of these is a file that ships, persists, or is shared,
so a value written there outlives the moment it was needed.

## Declare a secret plug

Add a `secret` plug to `sdkcraft.yaml`.
The plug has a name and an interface,
but no lookup attributes or value:

```yaml
name: secret-demo
# ...
plugs:
  api-key:
    interface: secret
```

Only regular SDKs can declare secret plugs.

Inside the workshop,
the request identifier is `<SDK>.<PLUG>`;
for an SDK named `secret-demo`
with the plug above,
the identifier is `secret-demo.api-key`.

**Workshop** never connects a secret plug automatically.
Document the plug name and the credential it expects
so users know which slot to create and connect.
The SDK must also handle the plug being unconnected
or the lookup failing.

## Connect the plug in a test workshop

The plug has nothing to request
until it’s connected to a secret slot.
Slots don’t ship with the SDK:
they belong to the `system` SDK,
and each user adds one to their workshop definition,
with lookup attributes that identify an item in their own keyring.
To test the SDK, take the same steps with a throwaway value.

Store the test value in the host keyring.
**secret-tool** prompts for the value;
enter the 23-byte `not-a-real-secret-value`
to match the output of later steps:

```console
$ secret-tool store --label="Secret demo test key" --collection=default service secret-demo account test
```

Build and install the SDK with **sdkcraft try**,
as described in [Try the SDK](https://ubuntu.com/workshop/docs/how-to/develop-sdks/build-an-sdk.md#how-build-sdk-try):

```console
$ sdkcraft try
```

In the test workshop definition,
add a `secret` slot to the `system` SDK
with attributes that match the stored item,
and list the SDK with the `try-` prefix:

```yaml
name: dev
base: ubuntu@24.04
sdks:
  - name: system
    slots:
      demo-key:
        interface: secret
        attributes:
          service: secret-demo
          account: test
  - name: try-secret-demo
```

Launch the workshop:

```console
$ workshop launch
```

Then connect the plug to the slot.
The plug keeps the SDK’s own name, without the `try-` prefix:

```console
$ workshop connect dev/secret-demo:api-key dev/system:demo-key
```

The connection persists when you rebuild the SDK
and refresh the workshop in later steps.

## Request a secret on demand

Run **workshopctl get-secret <SDK>.<PLUG>**
inside the workshop to request a connected secret:

```console
$ workshopctl get-secret <SDK>.<PLUG>
```

The command writes the value to standard output.
Capture or pipe that output directly into the consuming tool;
do not print it to a terminal or log.

For example,
the following check counts the bytes of the test value
without displaying it:

```console
$ workshop exec dev -- workshopctl get-secret secret-demo.api-key | wc -c

  23
```

When a request fails,
**workshopctl** writes a short diagnostic to standard error
and exits with a status that identifies the failure.
To see one, disconnect the plug:

```console
$ workshop disconnect dev/secret-demo:api-key
```

Then repeat the request;
an unconnected plug returns status 3:

```console
$ workshop exec dev -- workshopctl get-secret secret-demo.api-key

  secret plug "secret-demo.api-key" is not connected
```

Reconnect the plug before you continue:

```console
$ workshop connect dev/secret-demo:api-key dev/system:demo-key
```

The [workshopctl get-secret reference](https://ubuntu.com/workshop/docs/reference/cli/workshopctl.md#ref-workshopctl-cli)
lists every exit status.
Branch on the status when the SDK needs to distinguish
an unavailable provider, a missing item, multiple matches,
or an unconnected plug.

Every request also creates a `Retrieve secret` change.
The change and its tasks record whether delivery succeeded and why it failed,
but never contain the value.
Use **workshop changes** and **workshop tasks**
to troubleshoot an integration that hides the immediate error.

## Use a tool-native credential helper

Prefer a tool-native credential helper when one is available.
Configure the helper to run:

```console
$ workshopctl get-secret <SDK>.<PLUG>
```

The tool controls when it calls the helper
and how long it retains the returned value.
The value does not need to enter the tool’s environment.

For example,
Claude Code’s `apiKeyHelper` setting runs a command
and uses its output as the API key.

If the tool accepts the credential on standard input instead,
pipe the command output directly into the tool.
Keep shell tracing such as `set -x` disabled,
because it can print assignments and command substitutions.

## Wrap a tool that requires an environment variable

When a tool reads its credential only from an environment variable,
ship a wrapper that requests the value
and exports it only for the tool process.

The following wrapper starts `bin/secret-demo-tool`
with `SECRET_DEMO_API_KEY`.
A value already supplied by the user takes precedence:

```shell
#!/usr/bin/bash
# Runs secret-demo-tool with the API key from the connected secret plug.
# A key already set in the environment takes precedence.
sdk_dir=$(dirname "$(dirname "$(readlink -f "$0")")")

if [[ -z "${SECRET_DEMO_API_KEY:-}" ]]; then
  if key=$(workshopctl get-secret secret-demo.api-key); then
    export SECRET_DEMO_API_KEY="$key"
  fi
  unset key
fi

exec "$sdk_dir/bin/secret-demo-tool" "$@"
```

If the request fails,
**workshopctl get-secret** already writes the diagnostic.
The wrapper starts the tool without the variable,
allowing the tool to use another login mechanism
or report that the credential is missing.

If a silent fallback is intentional,
redirect standard error only for the request:

```shell
if key=$(workshopctl get-secret secret-demo.api-key 2>/dev/null); then
  export SECRET_DEMO_API_KEY="$key"
fi
```

The corresponding `Retrieve secret` change
remains available for troubleshooting while **Workshop** retains it.

Install the wrapper on `PATH`
from the SDK’s `setup-base` hook:

```shell
#!/usr/bin/bash
cat >/etc/profile.d/secret-demo.sh <<PROFILE
export PATH="${SDK}/bin:\$PATH"
PROFILE
```

Rebuild the SDK to include the wrapper and the hook:

```console
$ sdkcraft try
```

Refresh the test workshop to install the rebuilt SDK:

```console
$ workshop refresh
```

Then check that the tool receives the key:

```console
$ workshop exec dev -- secret-demo

  secret-demo-tool: authenticated with a 23-character key
```

Also test the unavailable path
by disconnecting the plug or locking the provider.
The wrapper must not turn a failed lookup into a success-shaped result.

## Deliver a secret to a systemd service

Use a systemd credential for a long-running service.
The service reads the credential from the file
named after it in `CREDENTIALS_DIRECTORY`.

For example,
this service process rejects a missing or empty credential
before starting its long-running work:

```shell
#!/usr/bin/bash
# Reads the API key from a systemd credential, then runs the service.
key_file="$CREDENTIALS_DIRECTORY/secret-demo.api-key"
if [[ ! -s "$key_file" ]]; then
  echo "secret-demo-service: no API key in the secret-demo.api-key credential" >&2
  exit 1
fi
echo "secret-demo-service: started with a $(wc -c <"$key_file")-byte key"
exec sleep infinity
```

Install the unit from `setup-base`.
Runtime hooks receive `SDK_SYSTEMD_SECRET_SOCKET`,
which is the socket systemd uses to request the value:

```shell
#!/usr/bin/bash
cat >/etc/profile.d/secret-demo.sh <<PROFILE
export PATH="${SDK}/bin:\$PATH"
PROFILE

cat >/etc/systemd/system/secret-demo.service <<UNIT
[Unit]
Description=Secret demo service

[Service]
User=workshop
LoadCredential=secret-demo.api-key:${SDK_SYSTEMD_SECRET_SOCKET}
ExecStart=${SDK}/bin/secret-demo-service
Restart=on-failure
RestartSec=30

[Install]
WantedBy=multi-user.target
UNIT

systemctl daemon-reload
systemctl enable --now secret-demo.service
```

Rebuild the SDK to include the service and the updated hook:

```console
$ sdkcraft try
```

Refresh the test workshop to install the rebuilt SDK
and start the service:

```console
$ workshop refresh
```

Each service start causes systemd to request the value again.
If the plug is unconnected or the lookup fails,
systemd still starts the unit with an empty credential.
Let the service reject that state,
and configure a restart policy or restart it after fixing the lookup.

Expect an empty credential in two cases:

- On initial launch,
  the service can start before the user connects the plug.
- During a refresh,
  an updated SDK’s `setup-base` hook runs
  before **Workshop** restores preserved connections.
  The first service start can therefore receive an empty credential.

The refresh you just ran is the second case.
To request the value immediately,
whether after a refresh, after connecting the plug,
or after correcting the lookup,
restart the service:

```console
$ workshop exec dev -- sudo systemctl restart secret-demo.service
```

Then check the service journal.
The start during the refresh rejected the empty credential,
and the restart received the key:

```console
$ workshop exec dev -- sudo journalctl --no-pager -u secret-demo.service

  ...
  Oct 13 10:41:57 dev secret-demo-service[488]: secret-demo-service: no API key in the secret-demo.api-key credential
  ...
  Oct 13 10:42:01 dev secret-demo-service[522]: secret-demo-service: started with a 23-byte key
```

When delivery fails,
inspect both the service journal
and the `workshop-secret@` journal:

```console
$ workshop exec dev -- sudo journalctl --no-pager -u secret-demo.service -u 'workshop-secret@*'
```

The credential lives in a systemd-managed file
for as long as the service runs.
The unit’s user and root can read it;
other processes running as the same user can also read the file.
This is not process-level isolation.

## Accept a secret for a single command

Users can also pass a credential ad hoc
to a single **workshop exec** or **workshop run** invocation,
without a slot or a connection.
The value stays off the command line:
users give the `--env` flag only the variable’s name
and fill the variable from a credential store on the host
with a command substitution:

```console
$ SECRET_DEMO_API_KEY=$(secret-tool lookup service secret-demo account test) workshop exec --env SECRET_DEMO_API_KEY dev -- secret-demo

  secret-demo-tool: authenticated with a 23-character key
```

Accept the value in the variable that the wrapper exports for the tool,
so that an ad hoc value overrides the connected plug:
the wrapper above gives an existing value precedence
and requests the connected secret only when the variable is absent.
If the named variable is unset,
**Workshop** skips it without an error,
and the wrapper falls back to the plug.

Document the variable’s name for users,
and tell them to pass only the name.
A value typed into `--env <NAME>=<VALUE>`
stays in the shell history,
and other processes on the host can read it from the process list
while the command runs.

## Review the exposure limits

Each delivery mechanism keeps the value out of SDK content,
but the value remains readable somewhere while it is in use:

| Mechanism                            | What can read the value                                                                       | What the SDK cannot clean up                                                                                                           |
|--------------------------------------|-----------------------------------------------------------------------------------------------|----------------------------------------------------------------------------------------------------------------------------------------|
| Tool-native helper or standard input | The process that consumes the command output<br/>and anything to which that process passes it | While the plug is connected,<br/>any process in the workshop can make a new request                                                    |
| Command wrapper                      | The tool and every process it starts,<br/>through the tool’s environment                      | While the plug is connected,<br/>any process in the workshop can request the value<br/>with **workshopctl get-secret**                 |
| Systemd credential                   | The unit’s user, root,<br/>and other processes running as that user                           | The credential file remains available<br/>until systemd stops the service                                                              |
| Single-command value                 | The command and every process it starts                                                       | A variable exported by the user’s shell<br/>or by a tool such as direnv<br/>stays available to every command run from that environment |

Disconnecting a plug prevents new requests,
but it cannot withdraw a value
that a process has already received.
Document this limitation for users,
especially when the SDK can run autonomous tools.

## See also

How-to guides:

- [How to declare plugs and slots](https://ubuntu.com/workshop/docs/how-to/develop-sdks/declare-plugs-slots.md#how-declare-plugs-slots)
- [How to provide secrets to a workshop](https://ubuntu.com/workshop/docs/how-to/develop-with-workshops/provide-secrets.md#how-provide-secrets)
- [How to write runtime hooks](https://ubuntu.com/workshop/docs/how-to/develop-sdks/write-runtime-hooks.md#how-write-runtime-hooks)

Reference:

- [workshop changes](https://ubuntu.com/workshop/docs/reference/cli/workshop.md#ref-workshop-changes)
- [workshop exec](https://ubuntu.com/workshop/docs/reference/cli/workshop.md#ref-workshop-exec)
- [workshop run](https://ubuntu.com/workshop/docs/reference/cli/workshop.md#ref-workshop-run)
- [workshop tasks](https://ubuntu.com/workshop/docs/reference/cli/workshop.md#ref-workshop-tasks)
- [workshopctl (CLI)](https://ubuntu.com/workshop/docs/reference/cli/workshopctl.md#ref-workshopctl-cli)
