Recommendations on using Juju secrets

TL;DR

  • share an ops.Secret object in your charm
  • try not to call model.get_secret() repeatedly
  • ensure that there’s only one ops.Secret object for a given secret

Prelude

I’ve put a good amount of work into how Juju secrets are handled in Ops at the end of the last cycle.

Primary motivation was multiple ops.Secret objects getting out of sync. What we had to do was to get rid of the caching layer in Ops. The layer was needed for Juju versions before 3.5.5, where the effects of running multiple hook commands within the same hook were not coalesced. The new solution is to map API calls to hook commands invocations directly for Juju 3.6 and later, and do a read-modify-write cycle for older Jujus.

The effort was complicated by the discovery of a range of Juju bugs, ranging from silly, to annoying, to unpredictable, to a vulnerability (under embargo).

Additionally, two fixes were landed in the state transition testing framework, ops[testing]:

  • output state secret object has the owner field stamped correctly now #2127
  • charm can now set and then read back the secret description #2115

We wanted to also change the implementation of model.get_secret(...), but ultimately decided not to proceed, as that would lead to either a change in observed behaviour, or a performance penalty.

Recap

  • When you call model.get_secret(...) the secret content is resolved immediately, which also means that your unit’s access to the secret is validated immediately.
  • When you receive an ops.Secret object from typed config, that is self.load_config(...), the content is not resolved and you have to call .get_content().
  • When you receive an ops.Secret object as an event argument, likewise the content is not resolved and you have to call .get_content().

The first of these easily leads to a situation where multiple ops.Secret objects aliasing the same underlying Juju secret may get out of sync.

We’re publishing these recommendations to help you get the best of Juju secrets in terms of robustness and performance.

Share the handle, not the data

When multiple parts of your charm have to be aware of the very same secret, share the ops.Secret object (the handle). Avoid sharing the data, as that is likely less secure.

Be aware of the secret backend cost

Looking up the secret via model.get_secret(...) incurs a network calls all the way to the secret backend configured for the current Juju model. That can be expensive if secrets are stored externally.

Additionally, multiple lookups, unless promptly disposed of, may lead to the aliasing problem described next.

Avoid aliased objects

Keeping several ops.Secret objects referencing the same Juju secret around leads to cases where the underlying secret is updated through one object and the change is not reflected in the other object.

2 Likes