Skip to content

feat: manage GPG credentials via Juju secrets#122

Open
jansdhillon wants to merge 10 commits intocanonical:mainfrom
jansdhillon:feat/gpg-juju-secrets
Open

feat: manage GPG credentials via Juju secrets#122
jansdhillon wants to merge 10 commits intocanonical:mainfrom
jansdhillon:feat/gpg-juju-secrets

Conversation

@jansdhillon
Copy link
Copy Markdown
Contributor

@jansdhillon jansdhillon commented Apr 22, 2026

Description of changes

Adds a gpg_secret_id config option that accepts a Juju secret URI (e.g. secret:xxxx). The secret must contain two fields:

  • gpg-passphrase: the GPG key passphrase
  • gpg-private-key: the ASCII-armored GPG private key

When configured, the charm:

  1. Writes the passphrase to /etc/landscape-server/gpg-passphrase.txt (mode 0600, owned by landscape:landscape)
  2. Imports the private key into /etc/landscape-server/gpg (mode 0700, owned by landscape:landscape) via gpg --import --passphrase-file
  3. Reacts to secret-changed to reconfigure and restart services when the secret is rotated

This is wired into both install and config-changed, and sets BlockedStatus with a descriptive message on any error (secret not found, missing fields, or GPG import failure).

Manual testing instructions

Create the Juju secret. Fields must be named exactly gpg-passphrase and gpg-private-key.

juju add-secret gpg-creds \
  gpg-passphrase=my-passphrase \
  gpg-private-key="$(cat my-private-key.asc)"

Grant the secret to the app and configure the charm with the URI returned above.

juju grant-secret gpg-creds landscape-server
juju config landscape-server gpg_secret_id=secret:<id>

Verify the passphrase file was written and has correct ownership/permissions.

juju exec --unit landscape-server/0 -- cat /etc/landscape-server/gpg-passphrase.txt
juju exec --unit landscape-server/0 -- stat -c "%U:%G %a" /etc/landscape-server/gpg-passphrase.txt

Expected: my-passphrase and landscape:landscape 600

Verify the key was imported into the GPG keyring.

juju exec --unit landscape-server/0 -- gpg --homedir /etc/landscape-server/gpg --list-secret-keys

Test secret rotation — update the secret and verify the charm reacts.

juju update-secret gpg-creds gpg-passphrase=new-passphrase gpg-private-key="$(cat new-key.asc)"
juju exec --unit landscape-server/0 -- cat /etc/landscape-server/gpg-passphrase.txt

Expected: new-passphrase

Test misconfiguration: secret not granted. Unit should enter BlockedStatus: "GPG secret not found or not accessible".

juju add-secret bad-secret gpg-passphrase=x gpg-private-key=y
juju config landscape-server gpg_secret_id=secret:<bad-id>

Test missing fields. Unit should enter BlockedStatus: "GPG secret missing required fields".

juju add-secret incomplete gpg-passphrase=x
juju grant-secret incomplete landscape-server
juju config landscape-server gpg_secret_id=secret:<incomplete-id>

Clear the config. Unit should return to ActiveStatus / WaitingStatus.

juju config landscape-server gpg_secret_id=

Adds gpg_secret_id config option that accepts a Juju secret URI
containing 'passphrase' and 'private-key' fields. When set, the
charm writes /etc/landscape/gpg-passphrase and imports the private
key into /etc/landscape/gpg on install and config-changed, and
reacts to secret-changed to reconfigure and restart services when
the secret is rotated.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adds support for managing Landscape Server’s GPG signing credentials via Juju secrets, allowing credentials to be retrieved/rotated at runtime and applied automatically by the charm.

Changes:

  • Introduces a new gpg_secret_id charm config option (Juju secret URI) and wires it into the charm config model.
  • Implements secret-backed GPG configuration in the charm (import private key into a dedicated homedir and write passphrase file), and reacts to secret-changed.
  • Adds unit tests covering the main GPG secret scenarios (missing secret, missing fields, import failure, and successful import).

Reviewed changes

Copilot reviewed 4 out of 4 changed files in this pull request and generated 4 comments.

File Description
src/charm.py Adds GPG secret retrieval/import + secret-changed handling and hooks it into install/config-changed.
src/config.py Extends the Pydantic config model with gpg_secret_id.
config.yaml Documents and exposes the new gpg_secret_id config option.
tests/unit/test_charm.py Adds unit tests for the new secret-driven GPG configuration flow.

Comment thread src/charm.py Outdated
Comment thread src/charm.py
Comment thread src/charm.py Outdated
Comment thread tests/unit/test_charm.py Outdated
jansdhillon and others added 2 commits April 22, 2026 13:49
- Use landscape gid (not root) for GPG_HOME_DIR and passphrase file ownership
- Rename secret fields to gpg-passphrase and gpg-private-key to match
  landscape-saas setup-gpg.sh convention
- Enforce GPG_HOME_DIR permissions with explicit chmod after makedirs
  to guard against umask interference
- Write passphrase file with restricted permissions from the start by
  setting umask to 0o177 before open(), eliminating the race window
  where the file could be world-readable
- Pass --passphrase-file to gpg --import for encrypted private keys
- Guard _on_install ActiveStatus against overriding a BlockedStatus
  set by _configure_gpg()
- Update tests to assert passphrase file path, permissions, and
  ownership; fix secret field names throughout

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Comment thread src/charm.py
Comment thread src/charm.py Outdated
jansdhillon and others added 5 commits April 22, 2026 14:14
- Downgrade secret-not-found and missing-fields log calls from
  error to warning since these are operator configuration issues,
  not program errors
- Set MaintenanceStatus at the top of _configure_gpg() and
  WaitingStatus on success, following the same pattern as the
  other _configure_* methods (_configure_oidc, _configure_openid)
- Move _configure_gpg() call in _on_install to after ActiveStatus
  is set, so it can naturally override with BlockedStatus on
  failure; _update_ready_status() then preserves that blocked state

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Remove explicit ActiveStatus before _configure_gpg(); instead let
_update_ready_status() determine final status, matching the pattern
used in _on_config_changed and other _configure_* functions.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
GPG home and passphrase paths should match the Landscape Server
config: /etc/landscape-server/gpg and /etc/landscape-server/gpg-passphrase.txt

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
ops raises ModelError (permission denied) when get_secret is called
for a secret that exists but hasn't been granted to the app.
Catch both SecretNotFoundError and ModelError for consistent BlockedStatus.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@jansdhillon
Copy link
Copy Markdown
Contributor Author

@copilot resolve the merge conflicts in this pull request

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants