Skip to content

feat(agent): support PSU gRPC app token secrets#1848

Merged
Benoît Cortier (CBenoit) merged 5 commits into
masterfrom
adamdriscoll-gateway-jwt-agent-tokens
Jul 9, 2026
Merged

feat(agent): support PSU gRPC app token secrets#1848
Benoît Cortier (CBenoit) merged 5 commits into
masterfrom
adamdriscoll-gateway-jwt-agent-tokens

Conversation

@adamdriscoll

Copy link
Copy Markdown
Contributor

PSU gRPC agents need to authenticate using configured application tokens while keeping secret-backed tokens out of static configuration. This change resolves the configured gRPC AppToken before opening the agent stream so literal tokens continue to pass through and $secret:<name> references reuse the existing PSU PowerShell secret resolver.

The implementation keeps token handling in the agent transport layer and avoids server-side or per-method parsing in this repository because the ASP.NET Core gRPC server and PSUPermissions integration are not present here. The gRPC request still carries the resolved token through standard authorization metadata for the PSU server authentication middleware to consume.

Validation:

  • cargo +nightly fmt --all
  • cargo test -p devolutions-agent psu_grpc_agent && cargo test -p devolutions-agent psu_event_hub::powershell_worker && cargo test -p devolutions-agent config
  • cargo clippy --workspace --tests -- -D warnings
  • git --no-pager diff --check
  • cargo test -p devolutions-agent

cargo test --workspace was attempted. The parallel run hit Windows paging-file/mmap errors; the single-job retry with ProgramData=C:\ProgramData progressed until devolutions-agent-updater required elevation (os error 740).

Resolve configured PSU gRPC AppToken values before opening the agent stream so literal tokens are passed through and secret references reuse the existing PowerShell secret resolver.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot AI review requested due to automatic review settings July 8, 2026 17:49
@github-actions

github-actions Bot commented Jul 8, 2026

Copy link
Copy Markdown

Let maintainers know that an action is required on their side

  • Add the label release-required Please cut a new release (Devolutions Gateway, Devolutions Agent, Jetsocat, PowerShell module) when you request a maintainer to cut a new release (Devolutions Gateway, Devolutions Agent, Jetsocat, PowerShell module)

  • Add the label release-blocker Follow-up is required before cutting a new release if a follow-up is required before cutting a new release

  • Add the label publish-required Please publish libraries (`Devolutions.Gateway.Utils`, OpenAPI clients, etc) when you request a maintainer to publish libraries (Devolutions.Gateway.Utils, OpenAPI clients, etc.)

  • Add the label publish-blocker Follow-up is required before publishing libraries if a follow-up is required before publishing libraries

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

This PR enables the PSU gRPC agent to authenticate with secret-backed application tokens. Previously, the gRPC agent forwarded the configured AppToken verbatim as bearer metadata, so a $secret:<name> reference would be sent literally instead of the resolved secret. The change resolves the token before opening the agent stream, reusing the existing PSU PowerShell secret resolver (already used by the PSU Event Hub compatibility feature) so literal tokens pass through unchanged and $secret:<name> references are resolved on demand. This fits into the devolutions-agent PSU integration, keeping token handling in the agent transport layer.

Changes:

  • Add PsuGrpcAgent::resolve_app_token and call it in run_single_connection so the resolved token (not the raw config value) is carried in authorization metadata.
  • Widen visibility of PowerShellWorker, its constructor/resolve_app_token, and the (renamed) app_token_secret_reference_name helper to pub(crate) so the gRPC agent module can reuse them.
  • Add unit tests covering literal-token pass-through and empty-token filtering.

Reviewed changes

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

File Description
devolutions-agent/src/psu_grpc_agent/mod.rs Adds resolve_app_token (literal vs $secret: handling), resolves the token before connecting, and adds two unit tests.
devolutions-agent/src/psu_event_hub/powershell_worker.rs Widens PowerShellWorker/new/resolve_app_token to pub(crate) and renames secret_reference_nameapp_token_secret_reference_name (crate-public); updates tests.
devolutions-agent/src/psu_event_hub/mod.rs Exposes the powershell_worker module as pub(crate) so the sibling gRPC agent module can reference it.

I verified all references to the renamed helper were updated, that widening visibility does not break existing consumers, that the new error messages follow the repo's lowercase/no-punctuation convention, and that the test struct literals match the dto definitions. No objective issues found.


💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

@CBenoit Benoît Cortier (CBenoit) left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Thank you Adam!


async fn run_single_connection(&self, shutdown_signal: &mut ShutdownSignal) -> anyhow::Result<()> {
let app_token = self.resolve_app_token().await?;
let endpoint = Endpoint::from_shared(self.server_url.clone())?;

@CBenoit Benoît Cortier (CBenoit) Jul 9, 2026

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

issue: I think app-token resolution runs on every reconnect, not once.
run_single_connection is called inside the retry loop in run() (in mod.rs).
For a $secret: token that means a fresh PowerShellWorker::new (temp .ps1 write) plus a pwsh subprocess spawn each cycle. "resolve ... before opening the agent stream" reads as a single pre-connect resolution, so this per-attempt behavior looks accidental. Contrast the event-hub path, which builds one shared worker and resolves once.

Probably another issue with this: because resolution sits inside the retried run_single_connection, a permanently misconfigured secret (missing vault, typo'd name…) is indistinguishable from a network failure: warn, back off, retry forever, spawning pwsh each cycle. The event-hub path instead logs and skips
the connection on resolution failure. For a single-connection agent "skip" would mean exiting the task, so retry may be the right call if the vault could come online later, but this deserves an explicit decision.

suggestion: Fix is small: resolve once in run() before the loop and pass the resolved token into
run_single_connection. Moving resolution out of the loop also lets a hard config error fail loudly once rather than looping quietly. If per-reconnect re-resolution is instead a deliberate secret-rotation feature, please signal the intent with inline comments.

thought: Also, if we want to distinguish which error are transients and which are fatal, we may consider returning something else than anyhow::Result. It’s generally a good idea when dealing with retry loops.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Addressed in 341fd1d

mod executor;
mod models;
mod powershell_worker;
pub(crate) mod powershell_worker;

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

thought: PowerShellWorker now has two consumers but still lives inside one of them. The gRPC transport now depends on the SignalR transport's internals for what is really a shared PSU concern (both spawn pwsh, both parse $secret:). Before this PR the two transports were independent siblings. This is the "make X reach into a sibling for a shared capability" shape that's usually better solved by extracting the shared piece: relocate powershell_worker (+ app_token_secret_reference_name) into a neutral module (e.g. psu_powershell, or
a psu parent both transports sit under). Note the worker also returns psu_event_hub::models::WebsocketEventResponse, so a clean extraction should carry/rename that type to something transport-neutral ("Websocket" is already a misnomer for a generic worker response).

Not blocking; happy to submit a follow up PR if you would rather not deal with that here.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Addressed in ed2673d


if app_token_secret_reference_name(app_token).is_none() {
return Ok(Some(app_token.to_owned()));
}

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

nitpick: This is a redundant $secret: check. PsuGrpcAgent::resolve_app_token checks app_token_secret_reference_name and PowerShellWorker::resolve_app_token checks it again.
Harmless, and this does buy skipping worker construction for literal tokens. Fine to keep; just flagging the double parse in case it’s unintended.

suggestion: A small inline comment would be appreciated if it’s intended 🙂

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Comment added: 6bdda93

Comment thread devolutions-agent/src/psu_powershell.rs Outdated
@@ -563,9 +563,9 @@ mod tests {

#[test]
fn secret_reference_name_is_case_insensitive() {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

nitpick: Stale test name after the rename.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Renamed here: 061d38f

@CBenoit Benoît Cortier (CBenoit) left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Thank you, LGTM!

@CBenoit Benoît Cortier (CBenoit) merged commit a9a2bb5 into master Jul 9, 2026
42 checks passed
@CBenoit Benoît Cortier (CBenoit) deleted the adamdriscoll-gateway-jwt-agent-tokens branch July 9, 2026 23:58
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Development

Successfully merging this pull request may close these issues.

3 participants