Skip to content

feat: self-signed HTTPS for goosed server#7126

Merged
aharvard merged 17 commits intomainfrom
aharvard/self-signed-https
Feb 25, 2026
Merged

feat: self-signed HTTPS for goosed server#7126
aharvard merged 17 commits intomainfrom
aharvard/self-signed-https

Conversation

@aharvard
Copy link
Copy Markdown
Collaborator

@aharvard aharvard commented Feb 10, 2026

Summary

Generate an ephemeral self-signed TLS certificate at startup so goosed serves over HTTPS. This gives MCP app iframes a secure context, enabling browser APIs (crypto.subtle, clipboard, etc.) and making 3rd-party iframe injection viable.

Changes

Rust (goose-server)

  • tls.rs (new) — Generates a self-signed cert for 127.0.0.1 + localhost using rcgen, installs the ring crypto provider, returns RustlsConfig
  • agent.rs — Switched from axum::serve (plain TCP) to axum_server::bind_rustls (TLS). Uses Handle for graceful shutdown.
  • lib.rs — Added pub mod tls;
  • Cargo.toml — Added rcgen 0.13 and axum-server 0.8 with tls-rustls feature
  • mcp_app_proxy.rs / mcp_app_proxy.html — Serve MCP app guest HTML from a real HTTPS URL instead of srcdoc, giving iframes a proper secure origin
  • tunnel/lapstone.rs — Made the proxy scheme configurable (https in production, http in tests). The reqwest client conditionally accepts self-signed certs when using HTTPS. Removed shared client from ProxyContext in favor of per-request client construction.
  • tunnel/mod.rs — Passes "https" scheme to lapstone::start

Electron (ui/desktop)

  • goosed.ts — Changed baseUrl from http:// to https:// (including external backend connections)
  • main.ts — Added setCertificateVerifyProc to accept self-signed certs from localhost, scoped trust via net.fetch instead of process-wide NODE_TLS_REJECT_UNAUTHORIZED=0, updated CSP connect-src to allow https://127.0.0.1:*

Tests

  • tests/integration/setup.ts — Set NODE_TLS_REJECT_UNAUTHORIZED=0 so Node.js integration tests accept the self-signed cert (in Electron this is handled by setCertificateVerifyProc)
  • lapstone_test.rs — Pass "http" scheme since the mock test server is plain HTTP

How It Works

  1. Startup: goosed calls tls::self_signed_config() which generates an ephemeral RSA key pair and X.509 cert with SANs for 127.0.0.1 and localhost
  2. Serving: axum_server::bind_rustls(addr, tls_config) serves all routes over HTTPS
  3. Electron renderer: setCertificateVerifyProc accepts the self-signed cert for localhost origins
  4. Electron main process: Uses net.fetch with scoped certificate trust for health checks
  5. MCP app iframes: Now load from https://127.0.0.1:{port}/mcp-app-proxy?secret=..., giving them a secure context ✅
  6. Tunnel proxy: Connects to local goosed via HTTPS with danger_accept_invalid_certs(true) (safe: only connects to our own 127.0.0.1)

Approach: HTTPS-only (Option 3)

Per review discussion, this PR implements Option 3 — HTTPS everywhere with scoped certificate acceptance. All callsites (tunnel proxy, external backend, Electron health checks, Node.js integration tests) were updated to use HTTPS. No --no-tls flag or dual HTTP+HTTPS stack needed.

Testing

  • cargo build -p goose-server
  • cargo fmt
  • cargo clippy -p goose-server
  • npm run typecheck (ui/desktop) ✅
  • Tunnel proxy tests pass with http scheme for plain HTTP mock server ✅
  • Node.js integration tests accept self-signed cert via NODE_TLS_REJECT_UNAUTHORIZED=0
  • Manual: launched app, confirmed goosed serves over HTTPS and MCP app iframes load

@michaelneale
Copy link
Copy Markdown
Collaborator

looks promising, if I read correctly, it is trying to let it slide for localhost, i wonder if there is a way to add the self cert to the trust store (just a thought if this doesn't work) but keep pushing!

@aharvard
Copy link
Copy Markdown
Collaborator Author

looks promising, if I read correctly, it is trying to let it slide for localhost, i wonder if there is a way to add the self cert to the trust store (just a thought if this doesn't work) but keep pushing!

In the last commit, I switched from the process-wide NODE_TLS_REJECT_UNAUTHORIZED=0 to using Electron's net.fetch for the goosed API client. net.fetch goes through Chromium's networking stack, which respects the certificate-error handler that's already scoped to localhost/127.0.0.1. This means external fetch calls (fetch-metadata, getAllowList) now retain strict TLS verification.

Re: adding the self-signed cert to the trust store — that's tricky since goosed regenerates the cert on every launch, so we'd need keychain writes on each startup. Happy to revisit if the current approach causes issues though.

Generate an ephemeral self-signed TLS certificate at startup so goosed
serves over HTTPS. This gives MCP app iframes a secure context, enabling
browser APIs (crypto.subtle, clipboard, etc.) and making 3rd-party
iframe injection viable.

Changes:
- Add tls module that generates a self-signed cert for 127.0.0.1/localhost
- Switch axum::serve to axum_server::bind_rustls for TLS termination
- Update Electron baseUrl from http to https
- Add certificate-error handler in Electron for localhost self-signed cert
- Set NODE_TLS_REJECT_UNAUTHORIZED=0 for main-process health checks
- Update CSP connect-src to allow https://127.0.0.1:*
MCP app guest iframes previously used srcdoc to inject HTML, which gave
them a URL of about:srcdoc. This caused third-party SDKs (like Square
Web Payments SDK) to fail because they check window.location.protocol
and reject anything that isn't https: or localhost.

This commit adds a /mcp-app-guest endpoint that stores guest HTML
server-side and serves it from a real https://localhost:{port} URL:

Server changes (mcp_app_proxy.rs):
- Added POST /mcp-app-guest to store HTML with a unique nonce
- Added GET /mcp-app-guest to retrieve and serve stored HTML with
  proper CSP headers (script-src, connect-src, frame-src, style-src)
- In-memory HashMap<String, StoredHtml> with 5-minute TTL and
  automatic cleanup on each request
- HTML is consumed on first retrieval (one-time use)

Proxy template changes (mcp_app_proxy.html):
- On receiving guest HTML via postMessage, the proxy now POSTs it
  to /mcp-app-guest to get a nonce, then sets the guest iframe src
  to GET /mcp-app-guest?secret=...&nonce=...
- Falls back to srcdoc if the endpoint is unavailable
- createGuestIframe is now async to support the fetch workflow

Auth changes (auth.rs):
- Added /mcp-app-guest to the auth exemption list (it uses its own
  secret query parameter for authentication, same as /mcp-app-proxy)

Desktop changes (goosed.ts, main.ts):
- Switched baseUrl from https://127.0.0.1:{port} to
  https://localhost:{port} so that SDKs with built-in localhost
  exceptions (like Square) recognize the origin
- Added http://localhost:* and https://localhost:* to the Electron
  CSP connect-src allowlist
- Updated default GOOSE_API_HOST to http://localhost
Replace process-wide NODE_TLS_REJECT_UNAUTHORIZED=0 with Electron's
net.fetch for the goosed API client. net.fetch uses Chromium's network
stack which respects the certificate-error handler already scoped to
localhost/127.0.0.1, so external fetch calls (fetch-metadata,
getAllowList) retain strict TLS verification.
The handler was inside appMain() but net.fetch needs it registered
before any createChat() call. Code paths like open-url and
second-instance can call createChat() before appMain() runs.
The certificate-error handler only applies to webContents (renderer)
requests. net.fetch in the main process needs
setCertificateVerifyProc on the default session to accept self-signed
certs from localhost. Registered via app.whenReady() before appMain()
to ensure it's active before any health check.
@aharvard aharvard force-pushed the aharvard/self-signed-https branch from e9929cc to 4ece61f Compare February 12, 2026 18:50
@aharvard
Copy link
Copy Markdown
Collaborator Author

@michaelneale I think we're ready for a review on this. Got anything else before I open it to broader feedback?

@michaelneale
Copy link
Copy Markdown
Collaborator

Looking good, also from research, this looks like http/2 which supports tons of streams and sse won't block, so this should I think help performance a lot as a side effect (what @DOsinga mentioned).

some things to consider:

(notes from my agents which I think I agree with):

File: crates/goose-server/src/tunnel/lapstone.rs, line 264

let url = format!("http://127.0.0.1:{}{}", port, message.path);

The tunnel proxy receives requests via WebSocket from Cloudflare and forwards them to the local goosed server using reqwest::Client::new() over plain HTTP. After this PR, goosed only serves HTTPS. The reqwest client will get a connection refused or TLS error when connecting to http://127.0.0.1:{port}.

Impact: The entire mobile tunnel feature breaks. All remote requests through the Cloudflare relay will fail with connection errors.

Fix needed: Either:

  1. Change to https://127.0.0.1:{port} and configure reqwest to accept self-signed certs (e.g., .danger_accept_invalid_certs(true))
  2. Or keep a plain HTTP listener alongside HTTPS (dual-stack)

External Backend Path Still Uses HTTP:

File: ui/desktop/src/goosed.ts, line 109

return connectToExternalBackend(dir, `http://127.0.0.1:${port}`);

When GOOSE_EXTERNAL_BACKEND is set, the URL is hardcoded to http://. If that external backend is also running HTTPS (because it's also running the updated goosed), this breaks. The fix should respect the protocol of the external server, or at least document the expectation.

I think these are only unexpected side effects, not sure of best way - perhaps we need a way to start goosed without self signed then both of these could work?

Very cool otherwise!

@aharvard
Copy link
Copy Markdown
Collaborator Author

@michaelneale Great catches on the tunnel and external backend — took your feedback and worked through the options with my agent. Here are the three paths we see:


Option 1: --no-tls flag

A CLI flag to start goosed in plain HTTP mode. The tunnel and external backend would use this mode.

  • ✅ Simple to implement — just a conditional at startup
  • ✅ No extra ports or listeners
  • ❌ Pushes complexity to callers — every spawn/connect site needs to know the mode
  • ❌ Easy to get wrong silently
  • ❌ Mobile tunnel users lose the secure context (MCP app iframes won't work)

Option 2: Dual-stack (HTTP + HTTPS)

Bind two listeners — HTTPS for Electron/iframes, HTTP for internal callers like the tunnel proxy.

  • ✅ Nothing breaks — existing HTTP callers keep working as-is
  • ✅ Electron gets its secure context
  • ❌ Two ports to manage and communicate
  • ❌ More surface area to reason about and document

Option 3: HTTPS-only, fix the callsites

Keep HTTPS-only and update the two broken spots:

  • lapstone.rs: switch to https:// + build a scoped reqwest client with .danger_accept_invalid_certs(true) (localhost-to-localhost, so safe)

  • goosed.ts: let the user provide the full URL in GOOSE_EXTERNAL_BACKEND instead of hardcoding the protocol

  • ✅ Cleanest end state — one protocol, one port, no modes

  • ✅ HTTP/2 benefits apply everywhere including the tunnel

  • ✅ Minimal code changes (a few lines per callsite)

  • ⚠️ Need to scope the cert-bypassing reqwest client tightly

  • ⚠️ External backend needs to handle the case where someone points at a plain HTTP server


I'm leaning toward Option 3 — it commits to the direction of the PR without adding escape hatches or parallel paths. The tunnel fix is ~5 lines of Rust, and making the external backend URL user-provided is arguably more flexible anyway.

Thoughts?

The tunnel proxy (lapstone) now builds a shared reqwest::Client with
danger_accept_invalid_certs(true) for localhost-only connections to the
self-signed goosed server. Introduced ProxyContext struct to bundle
per-connection state and fix clippy too-many-arguments lint.

The external backend path in goosed.ts now defaults to https:// to
match the normal startup path.
Resolved conflicts:
- Cargo.lock: took main's version, regenerated with cargo update
- agent.rs: kept OTLP shutdown logic from main
- goosed.ts: took main's refactored version, applied http->https fix
- main.ts: kept net.fetch client override for self-signed cert support,
  added createClient/createConfig imports
The tunnel proxy now uses HTTPS when connecting to the local goosed
server (which serves self-signed TLS), while tests use HTTP since the
mock server is plain HTTP. The reqwest client conditionally accepts
invalid certs only when scheme is https.
@aharvard
Copy link
Copy Markdown
Collaborator Author

Went with Option 3 (HTTPS-only, fix the callsites) — commits b4c0dd2 and bc82490.

What changed:

  • lapstone.rs: The tunnel proxy now connects to the local goosed server over HTTPS. The reqwest client is built per-request with danger_accept_invalid_certs(true) when the scheme is https (safe — it only ever connects to our own 127.0.0.1). The scheme is passed through startrun_single_connectionhandle_websocket_messageshandle_request.
  • goosed.ts: The external backend path now defaults to https:// to match the normal startup path.
  • Tests: Pass "http" as the scheme since the mock server is plain HTTP. This was the cause of the CI failures — the proxy was hitting the test server with HTTPS, getting empty responses, and panicking on JSON parse.

No --no-tls flag, no dual-stack — one protocol, one port, no modes. The tunnel gets HTTP/2 benefits as a side effect.

The goosed integration tests run in plain Node.js (not Electron), so
they need NODE_TLS_REJECT_UNAUTHORIZED=0 to accept the self-signed
TLS cert from the local goosed server. In the Electron app this is
handled by setCertificateVerifyProc.
Copy link
Copy Markdown
Contributor

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

This PR adds self-signed HTTPS support to the goosed server to provide a secure context for MCP app iframes. By serving over HTTPS, browser APIs like crypto.subtle and clipboard access become available to third-party iframe content, enabling richer MCP app functionality.

Changes:

  • Added TLS certificate generation at startup using rcgen and rustls
  • Migrated from axum::serve to axum_server::bind_rustls for HTTPS serving
  • Updated tunnel proxy to connect via HTTPS with self-signed certificate acceptance
  • Configured Electron to trust localhost self-signed certificates via setCertificateVerifyProc
  • Added server-side storage for guest HTML to provide real HTTPS URLs instead of srcdoc iframes

Reviewed changes

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

Show a summary per file
File Description
crates/goose-server/src/tls.rs New module that generates ephemeral self-signed certificates for localhost
crates/goose-server/src/commands/agent.rs Switched to HTTPS serving using axum_server::bind_rustls with graceful shutdown
crates/goose-server/src/tunnel/lapstone.rs Made proxy scheme configurable, accepts self-signed certs for HTTPS connections
crates/goose-server/src/tunnel/mod.rs Passes "https" scheme to tunnel proxy
crates/goose-server/src/tunnel/lapstone_test.rs Uses "http" scheme for plain HTTP test servers
crates/goose-server/src/routes/mcp_app_proxy.rs Added endpoints to store/serve guest HTML with real HTTPS URLs
crates/goose-server/src/routes/templates/mcp_app_proxy.html Client-side logic to store guest HTML server-side and load via real URL
crates/goose-server/src/auth.rs Added /mcp-app-guest to auth bypass list (auth still checked in handler)
crates/goose-server/src/lib.rs Exported new tls module
crates/goose-server/Cargo.toml Added rcgen and axum-server dependencies
ui/desktop/src/main.ts Added certificate trust for localhost, recreated client with net.fetch, updated CSP
ui/desktop/src/goosed.ts Changed base URLs from http:// to https://
ui/desktop/tests/integration/setup.ts Set NODE_TLS_REJECT_UNAUTHORIZED=0 for integration tests
Cargo.lock Dependency lock file updates

Adds ensure_extensions_loaded() helper that awaits any in-flight background
extension loading task before serving read_resource or call_tool requests.
This prevents 500s when the UI hits these endpoints before start_agent's
background spawn has finished registering extensions.

Also adds error logging to read_resource so failures aren't silently swallowed.
Copilot AI review requested due to automatic review settings February 19, 2026 19:36
Copy link
Copy Markdown
Contributor

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

Copilot reviewed 14 out of 15 changed files in this pull request and generated 2 comments.

Comments suppressed due to low confidence (2)

crates/goose-server/src/tunnel/lapstone.rs:276

  • The error message "failed to build reqwest client" doesn't provide context about what failed or why. Consider using context() from anyhow to provide more details, or handle the error properly and propagate it to the caller.
        .expect("failed to build reqwest client");

crates/goose-server/src/routes/templates/mcp_app_proxy.html:71

  • The function signature changed from synchronous to asynchronous but the function name wasn't updated to reflect this. Consider renaming to indicate the async nature, or document the change with a comment explaining why the async conversion was necessary.
        async function createGuestIframe(html, permissions) {

// setCertificateVerifyProc handles net.fetch requests (main process).
app.on('certificate-error', (event, _webContents, url, _error, _certificate, callback) => {
const parsed = new URL(url);
if (parsed.hostname === '127.0.0.1' || parsed.hostname === 'localhost') {
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

This seems risky if MCP apps will be used for anything sensitive. (Will they?) A malicious local process could race to serve on the same port and would be accepted no matter what cert it presents.

This is no worse than before, when we didn't use HTTPS at all, but there's a reason browsers only enable certain features when HTTPS is used, and we are circumventing that protection. I think the improvement would be reasonably simple: goosed could emit the generated cert's fingerprint to stdout and we could grab it and accept only that cert here. If we choose not to do this, I think a comment acknowledging the decision to leave the cert validation open would be a good idea.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

The main driver of this change was to enable MCP Apps to accept payments via the Square Web Payments SDK, So yeah, I think that's sensitive. :)

Great idea. I'll get that set up!

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Done — goosed now emits GOOSED_CERT_FINGERPRINT=<sha256-hex> to stdout at startup. The Electron app captures it and pins it in both certificate-error and setCertificateVerifyProc. Before the fingerprint arrives during early startup, localhost certs are accepted to allow the initial health check; once pinned, only the exact cert is accepted.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Sorry to nitpick but maybe we could use the url passed to the certificate-error event to just always allow the health check through with any cert, and not have it fail open if anything goes wrong with passing the fingerprint across?

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Good call. Updated — certificate-error now always accepts localhost (URL-only check), so renderer page loads and iframe resources can never fail due to fingerprint timing. Fingerprint pinning is scoped to setCertificateVerifyProc only, which handles net.fetch from the main process where the security benefit actually applies.

/// return an `axum_server::tls_rustls::RustlsConfig` ready to use.
pub async fn self_signed_config() -> Result<RustlsConfig> {
// rustls 0.23+ requires an explicit crypto provider installation.
let _ = rustls::crypto::ring::default_provider().install_default();
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

nit: I think we're using awc_lc_rs elsewhere; using it for rustls as well may possibly cut down the crate graph a little

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Good call — switched both the rustls crypto provider and the digest to aws_lc_rs, and removed the direct ring dependency

return Ok(());
}
};
let mut client_builder = reqwest::Client::builder();
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Can we re-use the reqwest::Client rather than building a new one for every request?

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Fixed — the client is now built once in run_single_connection and stored in ProxyContext so it's reused across all requests for the lifetime of the tunnel connection.

/// In-memory store for guest HTML content.
/// Maps nonce -> (html_content, csp_string)
/// Entries are consumed on first read (one-time use).
type GuestHtmlStore = Arc<RwLock<HashMap<String, (String, String)>>>;
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

As far as I can see there's no bound on this store; if the client doesn't read the response body the entry will be left behind. If every possible client who can cause an entry to be written here is trusted then this is just a nit.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Added a 5-minute TTL and 64-entry cap. Expired entries are evicted on each write, and if still at capacity the oldest entry is dropped.

goosed now emits GOOSED_CERT_FINGERPRINT=<sha256-hex> to stdout at
startup. The Electron app captures it and pins it in both
certificate-error and setCertificateVerifyProc handlers, so only the
exact cert generated by this goosed instance is accepted.

Before the fingerprint arrives (during early startup), localhost certs
are accepted to allow the initial health check. Once pinned, any other
self-signed cert from localhost is rejected.

Addresses jh-block review feedback on PR #7126.
- Switch from ring to aws_lc_rs for rustls crypto provider and SHA-256
  digest, reducing duplicate crypto backends in the crate graph
- Reuse reqwest::Client in tunnel proxy by storing it in ProxyContext
  instead of building a new one per request
- Bound the guest HTML store with a 5-minute TTL and 64-entry cap,
  evicting expired/oldest entries on each write
@aharvard
Copy link
Copy Markdown
Collaborator Author

@jh-block thanks for the review, can you PTAL at my updates? Want to make sure I'm on track before merge.

@aharvard aharvard requested a review from jh-block February 20, 2026 16:23
@jh-block
Copy link
Copy Markdown
Collaborator

All looks great but I had one further comment on the certificate verification part, I think it's also fine to punt that especially if it's not as easy as I imagined

…ly in setCertificateVerifyProc

Per jh-block's follow-up: certificate-error (renderer webContents) now
always accepts localhost so page loads and iframe resources never fail.
Fingerprint pinning is scoped to setCertificateVerifyProc (main-process
net.fetch) where the security benefit applies.
Copilot AI review requested due to automatic review settings February 20, 2026 17:07
Copy link
Copy Markdown
Contributor

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

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

Comments suppressed due to low confidence (1)

crates/goose-server/src/commands/agent.rs:52

  • The tls_setup.fingerprint field is returned but never used in this function. The fingerprint is printed to stdout (line 41 in tls.rs) for the Electron process to capture, so this field in the returned struct serves no purpose here.
    let tls_setup = self_signed_config().await?;

@aharvard
Copy link
Copy Markdown
Collaborator Author

All looks great but I had one further comment on the certificate verification part, I think it's also fine to punt that especially if it's not as easy as I imagined

seemed pretty easy to implement @jh-block, whatcha think?

@jh-block
Copy link
Copy Markdown
Collaborator

sorry took a minute to circle back here. The implementation is slightly different than what I had in mind. It's not clear to me that the renderer is lower-risk than the main process (an attacker being able to cause different content to be rendered is certainly a problem). So always accepting any cert from localhost on the renderer seems risky. I was imagining we would only accept any cert for the health check, and keep the pin in place for everything else, to close the hole for both main-process stuff and for the renderer.

Otherwise everything looks great!

The certificate-error handler (renderer) was accepting any self-signed
cert from localhost, while setCertificateVerifyProc (main process) was
already pinning to the exact fingerprint. An attacker who could MITM
localhost with a different self-signed cert could serve malicious content
in the renderer.

Now both handlers share the same logic: accept any localhost cert only
during the brief bootstrap window before the fingerprint is known, then
pin to the exact cert goosed generated.

Also adds missing Buffer import from node:buffer to fix pre-existing
eslint no-undef error in normalizeFingerprint.
Keep both the gateway_manager auto-start spawn (from main) and the
axum_server::bind_rustls TLS binding (from this branch).
@aharvard aharvard added this pull request to the merge queue Feb 25, 2026
Merged via the queue into main with commit 785818b Feb 25, 2026
22 checks passed
@aharvard aharvard deleted the aharvard/self-signed-https branch February 25, 2026 19:55
craigwalkeruk pushed a commit to craigwalkeruk/custom-goose that referenced this pull request Mar 5, 2026
wpfleger96 added a commit that referenced this pull request Mar 6, 2026
PR #7126 hardcoded self-signed HTTPS as the only server mode, breaking
headless deployments (K8s health probes, sidecar proxies, plain HTTP
clients) that expect HTTP. TLS is now controlled by the GOOSE_TLS env
var (default: true), so existing desktop deployments are unaffected and
server deployments can opt out with GOOSE_TLS=false.
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.

4 participants