Repository navigation
Description
Summary
Configured base URLs of Docker instances and Portainer are written verbatim into log messages, exception and scan-run error messages, the database and the Docker Instances pages, and the HTTP retry log keeps the user-info part of request URLs. An operator who configures a URL with embedded credentials (https://user:secret@host) therefore leaks that secret to the console log, exported OTLP logs, stored scan-run errors and every visitor of the UI. User info must be stripped or masked everywhere a URL leaves configuration.
Area
docs/areas/telemetry.md(added in [Docs] Describe the current behavior by area #263) — Credentials and external data, Open question 3docs/areas/web-ui.md(added in [Docs] Describe the current behavior by area #263) — Secrets, Open question 3docs/areas/docker-instances.md,docs/areas/container-actions.md,docs/areas/registries.md(log and error messages of the clients)- Security areas (
.squad/project.md): Secrets in configuration (never logged, never shown in the UI, never in exception messages or telemetry attributes) and Logging and telemetry of external data; also touches Registry and Docker Hub calls (TransientHttpRetryHandler) and Container actions (PortainerClient).
Current behavior
Verified locally: new Uri("https://user:secret@portainer.example:9443/api/auth?x=1").GetLeftPart(UriPartial.Path) returns https://user:secret@portainer.example:9443/api/auth; Uri.UserInfo of tcp://user:secret@docker.example:2376 is user:secret. Configuration validation accepts URLs with user info (Configuration/DockerUpdateGuardOptionsValidator.cs:299-305, :327-339).
All paths below are under src/DockerUpdateGuard/.
Logs
Infrastructure/TransientHttpRetryHandler.cs:120-135—GetRequestTargetdrops only the query string;GetLeftPart(UriPartial.Path)keeps user info. Used by events3500/3501(Infrastructure/TransientHttpRetryHandlerLogging.cs:20-49,{RequestTarget}).Docker/DockerInstanceClientLogging.cs:26-31event3101"Docker instance {DockerInstanceName} uses unsupported endpoint {BaseUrl}", called atDocker/DockerInstanceClient.cs:983withinstanceOptions.BaseUrl.Images/ImageHostLoggingExtensions.cs:612-617"Docker instance {DockerInstanceName} uses unsupported connection settings for endpoint {BaseUrl}", called atImages/InstanceDiscoveryService.cs:118withconfiguredInstance.BaseUrl.Portainer/PortainerClientLogging.cs:25-28event3303"Portainer authentication failed for {BaseUrl}", called atPortainer/PortainerClient.cs:166,173withoptions.BaseUrl.
Exception and result messages (end up in logs and, for scans, in ScanRun.ErrorMessage, which the dashboard, scan history and detail pages show)
Portainer/PortainerClient.cs:168,175—InvalidOperationException($"… for '{options.BaseUrl}'").Docker/DockerInstanceClient.cs:985,1063,1152,1189,1223—ExternalOperationResult<…>.Unsupported($"Docker instance '{…Name}' uses an unsupported endpoint '{instanceOptions.BaseUrl}'").
Database and UI
Images/InstanceDiscoveryService.cs:109storesconfiguredInstance.BaseUrl.Trim()inDockerInstance.EndpointUri;:155stores the PortainerBaseUrlinPortainerEndpoint.BaseUrl. Neither stored value is used for connecting (the clients use the configured options);EndpointUriexists for display.UI/ApplicationViewService.cs:2101,2148mapEndpointUrito the view data, rendered atComponents/Pages/DockerInstances.razor:31,137andComponents/Pages/DockerInstanceDetail.razor:23.
Reproduction: configure DockerUpdateGuard:DockerInstances:0:Portainer:BaseUrl = https://admin:S3cret@portainer.example:9443 with wrong credentials; on the first Portainer call the log contains Portainer authentication failed for https://admin:S3cret@portainer.example:9443. Configure a Docker instance BaseUrl = https://user:S3cret@docker.example:2376; the Docker Instances page shows the URL including user:S3cret, and a transient 5xx from it logs Transient HTTP response 503 for GET https://user:S3cret@docker.example:2376/v1.41/containers/json; retrying ….
Expected behavior
- No log message, log attribute, span attribute, exception message,
ExternalOperationResultmessage, scan-run error message, stored display value or UI text contains the user-info part of a URL. - Wherever a URL with user info would appear, it appears with the user info replaced by
***(e.g.https://***@portainer.example:9443), so operators can still see that credentials are configured. URLs without user info are shown unchanged (includingunix:///var/run/docker.sockandnpipe://…). - The retry log additionally keeps omitting the query string (unchanged).
- Strings that are not parseable as absolute URIs but contain a
scheme://userinfo@prefix are masked the same way (the unsupported-endpoint paths log exactly such strings). DockerInstance.EndpointUriandPortainerEndpoint.BaseUrlare stored masked; the UI masks again on display so rows written before the fix (e.g. of instances no longer in configuration, which synchronization does not update) never show credentials.- Connecting still uses the configured URL as is (masking is for output only).
- The OpenTelemetry HTTP client spans (
url.full) and the ASP.NET Core spans must not carry user info either (the HTTP client instrumentation is expected to strip it already; verify).
Acceptance criteria
- One helper (e.g.
Infrastructure/UrlRedaction.cswithstring RedactUserInfo(string? url)and an overload forUri) is used at every site listed under Current behavior. - Unit tests for the helper (
UrlRedactionTests):UrlRedactionRedactUserInfoUserAndPasswordMasked,…UserOnlyMasked,…NoUserInfoUnchanged,…UnixSocketUnchanged,…UnparsableWithUserInfoMasked,…NullOrEmptyReturnsEmpty,…QueryStringPreserved(retry handler removes the query separately). -
TransientHttpRetryHandlerTests: a retried request tohttps://user:secret@host/path?token=xlogshttps://***@host/path(no user info, no query). -
PortainerClientTests: failed authentication logs and throws without the user info. -
DockerInstanceClientTests: unsupported endpoint result message and log contain no user info. -
InstanceDiscoveryServiceTests: storedEndpointUriandPortainerEndpoint.BaseUrlare masked. -
DockerInstancesRenderTests/ApplicationViewServiceTests: an endpoint stored unmasked (legacy row) is displayed masked. - A test (or documented manual check in the PR) confirms that
url.fullof HTTP client spans contains no user info. - Tests use the existing
TestLogger/fakes; assertions check the absence of the secret string, with messages. - Coverage gate passes; analyzer gate clean.
- The area documents
docs/areas/telemetry.mdanddocs/areas/web-ui.mddescribe the new behavior (URLs appear without user info; endpoints are shown masked) and their Known gaps entries for this issue are removed;docs/ARCHITECTURE.mdSecurity posture updated if it states what logs carry.
Implementation notes
- Prefer
Uri.GetComponents(UriComponents.SchemeAndServer | UriComponents.Path, UriFormat.UriEscaped)plus re-inserting***@whenUserInfois not empty, over string surgery; fall back to a regex^([A-Za-z][A-Za-z0-9+.-]*://)[^/@]*@→$1***@for unparsable input. - Apply masking at the boundary (the log/exception/result call and the store/display mapping), not by changing the options objects — the clients must keep connecting with the configured URL.
- Masking on write to the database is enough for active instances because synchronization rewrites
EndpointUri/BaseUrlon every run (InstanceDiscoveryService.cs:109,155); masking on display covers stale rows. No migration needed. - Search for further URL output while implementing (
BaseUrl,EndpointUri,RequestUri,GetLeftPart) — the list above is complete as of this analysis, but new sites must use the helper too. - Must not change: event IDs and message templates' parameter names (dashboards rely on them; see
docs/areas/telemetry.md), the rule that validation messages name keys and instance names only,AllowInsecureHttp/SkipCertificateValidationwarnings. - This is a security-tier change (secrets in logs/UI): Security reviews the plan and the diff.
Related
- PR [Docs] Describe the current behavior by area #263 (area documents)
- F-012 (P1) — Image environment variables (potential secrets) are read #34 (image environment variables read — secrets) — another path by which secrets could reach storage/UI.
- F-041 (P6) — Dead telemetry name constants:
TelemetryLogPropertyNamesentirely unused #84 (dead telemetry log property names) — same logging surface.
Generated by Claude Code
Activity
LarsLaskowski commentedon Oct 24, 2026
Squad working record
Tier: security. Plan: specs/issue-295/plan.md (permalink to the last commit that contains it). Decision record 0005 (accepted). PR: #305.
Summary. One helper,
UrlRedaction.RedactUserInfo, replaces user info with***at every output site (retry log, Docker/Portainer logs, exception and result messages, storedEndpointUri/PortainerEndpoint.BaseUrl, UI mapping). It rebuilds a URL only when it has exactly one@, parses, starts withscheme://and has non-empty user info; every other value containing@is masked fail-closed up to its last@. This replaces the issue's regex suggestion, which would have leaked forms likehttps://user:p@ss@hostor passwords starting with#,?or/. Extra sites found beyond the issue: the named-pipe exception message and the storedBaseUrlof a disabled Portainer endpoint.url.fullof HTTP client spans carries no user info (checked in the OpenTelemetry and runtime sources). The Docker Engine client has no retry handler, so the issue's Docker retry example cannot occur. Challenge: 0 major, 1 minor (answered). Security plan review: one revision (empty-user-info branch), then approved; Security diff review approved. Follow-ups: #304 (Trivy error output). Squad lessons: #306.log.md
Squad run metrics
Generated by Claude Code