Skip to content

Tailscale Funnel as a new Service Mode tunnel option - #2378

Open
mnaqvi-ks wants to merge 23 commits into
feature/tailscale-funnel-service-modefrom
feature/tailscale-funnel-service-mode-int
Open

mnaqvi-ks wants to merge 23 commits into
feature/tailscale-funnel-service-modefrom
feature/tailscale-funnel-service-mode-int

Conversation

@mnaqvi-ks

Copy link
Copy Markdown

Adds Tailscale Funnel as a new tunnel option for Commander Service Mode, alongside the existing Ngrok and Cloudflare integrations. Users can now expose the Service Mode REST API over a public HTTPS URL using their own Tailscale account, with no changes to how Ngrok/Cloudflare already work.

# Plain personal auth key
keeper service-create -p 8090 -ts '<tailscale-auth-key>' -rm foreground -q y -f yaml -c "ls"

# OAuth-issued auth key (needs the ACL tag it's scoped to)
keeper service-create -p 8090 -ts '<oauth-client-secret>' -tst 'tag:commander-service' -rm foreground -q y -f yaml -c "ls"

Features

  • New tunnel provider — -ts, --tailscale <auth-key> / -tst, --tailscale_advertise_tags <tags>, following the same single-flag convention as -ng/-cf. Mutually exclusive with Ngrok/Cloudflare.
  • CLI detection & auto-install — detects a missing Tailscale CLI and, with explicit user confirmation, installs it automatically (Homebrew on macOS, official script on Linux, MSI with UAC elevation on Windows). Falls back to manual guidance if declined or if it fails.
  • Daemon detection & auto-start — same consent-gated flow if tailscaled isn't running.
  • Authentication — supports both plain personal auth keys and OAuth-client-issued keys. Used only for tailscale up; never touches the Service Mode API Key, which continues to gate every API request unchanged.
  • Full Funnel lifecycle — start, live status, and reset wired into service-create/service-start/service-status/service-stop. Startup failures roll back cleanly (including already-started Ngrok/Cloudflare processes).
  • Public URL retrieval — real HTTPS URL pulled from the node's Tailscale hostname; printed on create, persisted to config, reported via service-status, and included in Docker vault-record metadata (-ur).
  • Security — auth key passed via a short-lived, owner-only temp file (--auth-key=file:...) instead of a raw CLI argument, avoiding exposure via ps to other local users. Never logged.

Design notes

  • service-stop only resets the Funnel exposure — it doesn't log the machine out of Tailscale or stop the daemon, since tailscaled is system-wide and may be used for other things on the same machine.
  • CLI flag naming (-ts <key> as a single flag) intentionally matches the existing -ng/-cf convention rather than a separate enable+key pair.

Files changed

File Purpose
keepercommander/service/config/tailscale_config.py (new) Orchestrates CLI/daemon checks, authentication, Funnel start, and URL retrieval — the Tailscale equivalent of ngrok_config.py/cloudflare_config.py
keepercommander/service/util/tunneling.py Adds all low-level tailscale CLI wrapper functions (install, daemon start, auth, Funnel start/stop/status, URL lookup)
keepercommander/service/core/service_manager.py Wires Tailscale into start_service/stop_service/get_status, including rollback-on-failure and interrupt handling
keepercommander/service/core/process_info.py Adds tailscale_enabled/tailscale_port lifecycle tracking (no PID — Tailscale has no Commander-owned subprocess)
keepercommander/service/core/globals.py Transient, same-process global for deferring vault-metadata write-back until the real Funnel URL is known
keepercommander/service/commands/create_service.py Adds -ts/-tst CLI flags and config schema fields
keepercommander/service/commands/service_config_handlers.py Interactive and streamlined config prompts/assembly for Tailscale, extending the existing mutual-exclusion chain
keepercommander/service/commands/integrations/vault_metadata.py Extracts the shared get_service_url() helper; fixes Docker metadata write-back timing so it waits for the real URL
keepercommander/service/config/config_validation.py Adds validate_tailscale_auth_key
keepercommander/service/config/models.py / service_config.py Adds tailscale, tailscale_auth_key, tailscale_advertise_tags, tailscale_public_url config fields, defaults, and backward-compat migration
keepercommander/resources/service_config.ini Adds interactive prompt text for Tailscale
keepercommander/service/README.md Documents the new flags, examples, and the --force-reauth/single-use-key note
unit-tests/service/test_create_service.py Tests for the new CLI flag behavior, mutual exclusion, and streamlined config assembly

Compatibility

No changes to ngrok_config.py or cloudflare_config.py. All changes to shared files are additive, confirmed via diff review. Full existing test suite (279 tests) passes unchanged.

- Introduced Tailscale configuration and management in the service.
- Added functions for installing, starting, and managing Tailscale daemons and funnels.
- Enhanced ProcessInfo to track Tailscale status and ports.
- Updated ServiceManager to handle Tailscale alongside existing tunneling options (ngrok, Cloudflare).
- Implemented validation for Tailscale configuration parameters.
- Created a new TailscaleConfigurator class for managing Tailscale setup and validation.
- Added logging for Tailscale subprocess activities.
- Updated unit tests to cover new Tailscale functionality.
- Introduced `tailscale_advertise_tags` to ServiceConfig and related classes.
- Updated ServiceConfigHandler to handle new argument for Tailscale.
- Modified TailscaleConfigurator to utilize advertise tags during authentication.
- Adjusted validation logic to check for presence of Tailscale auth key and tags.
- Enhanced installation and daemon management functions to ensure proper handling of Tailscale.
- Updated unit tests to cover new `tailscale_advertise_tags` parameter.
sshrushanth-ks and others added 13 commits September 18, 2026 08:05
…2365) (#2379)

* Fix Gateway Name not displayed in pam rotation info output

* Add UID type normalization and graceful error handling to gateway name resolution

* Improve gateway name resolution: add UID validation and specific exception logging

* Prevent None == None false positive by explicitly checking target_uid before gateway lookup
#2372)

* Block Service Mode from ever accessing its own config records

* Fix copilot review comments

* Fix claude review comments

* Fix review comments
…ess (#2383) (#2385)

* Protect Integration config records from Service Mode API access

* Add terraform backward compatability and test cases

* Fix review comments

* Fix failing test case in windows

* Restrict folder access via Service Mode for all *-setup commands

* Prevent config and service config json file attachments

* Fix claude review comments
- Introduced Tailscale configuration and management in the service.
- Added functions for installing, starting, and managing Tailscale daemons and funnels.
- Enhanced ProcessInfo to track Tailscale status and ports.
- Updated ServiceManager to handle Tailscale alongside existing tunneling options (ngrok, Cloudflare).
- Implemented validation for Tailscale configuration parameters.
- Created a new TailscaleConfigurator class for managing Tailscale setup and validation.
- Added logging for Tailscale subprocess activities.
- Updated unit tests to cover new Tailscale functionality.
- Introduced `tailscale_advertise_tags` to ServiceConfig and related classes.
- Updated ServiceConfigHandler to handle new argument for Tailscale.
- Modified TailscaleConfigurator to utilize advertise tags during authentication.
- Adjusted validation logic to check for presence of Tailscale auth key and tags.
- Enhanced installation and daemon management functions to ensure proper handling of Tailscale.
- Updated unit tests to cover new `tailscale_advertise_tags` parameter.

except Exception as e:
logger.error(f"Failed to start service subprocess: {e}")
raise

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Tailscale Funnel is started before the Commander service subprocess is successfully created. If spawn_detached_process() or ProcessInfo.save() fails here, the exception path does not stop the Tailscale Funnel that was already configured at lines 118-120. This can leave the Funnel publicly exposed while the Commander service is not running. Can we add Tailscale cleanup to this rollback path?

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

fixed, the outer exception handler now stops the Funnel if it was already up before the subprocess/Flask app failed to start.

Comment on lines 566 to 568
except psutil.NoSuchProcess:
ProcessInfo.clear()
pass

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

When the Commander process is no longer running, this clears ProcessInfo but leaves the Tailscale Funnel configuration active. Since Funnel is managed by tailscaled and not tied to the Commander process lifecycle, an unexpected Commander crash/SIGKILL can leave the public Funnel endpoint enabled. Should we reconcile/disable the Commander-managed Funnel when detecting that the service process is gone?

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Fixed — get_status() now stops the Funnel too when it detects the process is gone, not just on an explicit service-stop

# Tailscale Funnel only accepts one of these as the external-facing port;
# the local target port (the Commander service port) is unrestricted and
# separate. 443 is the default so the public URL needs no port suffix.
TAILSCALE_FUNNEL_ALLOWED_PORTS = (443, 8443, 10000)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

TAILSCALE_FUNNEL_ALLOWED_PORTS defines the supported external Funnel ports, but start_tailscale_funnel() doesn't validate funnel_port against it. Should we validate the port here before invoking the CLI, or remove the unused constant?

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Added validation using the existing constant instead of removing it. The parameter is retained for future extensibility and keeps the signature consistent with get_tailscale_funnel_url and stop_tailscale_funnel, so validating it now prevents invalid values from reaching the Tailscale CLI and failing there.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Do not commit log file and log folder

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Sorry for that
fixed


guidance = guidance_fn()
logger.error(guidance)
print(guidance)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Can you explain why guidance is printed?

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

This flow handles two checks: the Tailscale CLI being missing and the daemon not running. In either case, we show the manual fix before offering auto-fix, so the user always knows what to do if they decline or auto-fix fails
Guidance here printed for the manual fix

This branch has not been deployed

No deployments
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.

7 participants