Deploying from the CLI behind an auth proxy¶
When ShinyHub sits behind an authentication proxy (Authelia, oauth2-proxy, Cloudflare Access, or any other forward-auth solution), browser users authenticate at the edge before their requests ever reach ShinyHub. That flow is browser-only: the proxy redirects unauthenticated requests to a login page and exchanges cookies or tokens through the browser.
The CLI (shinyhub deploy, shinyhub env, shinyhub apps, and every other
subcommand) is not a browser. It cannot follow an interactive redirect, complete
an OAuth dance, or satisfy a CAPTCHA. A CLI request to the proxied hostname gets
bounced to the auth provider's login page and returns an unexpected HTML response
instead of the JSON the CLI expects.
The fix: target the app port directly¶
For CLI and CI operations, bypass the proxy entirely. Talk directly to the
ShinyHub port (default :8080) and authenticate with a service-account
credential. This deploys applications and fleets into ShinyHub; it does not
deploy or upgrade the ShinyHub server itself.
Browser --> Proxy (auth) --> ShinyHub :8080
^
|
CLI / CI --------+ (bypasses the proxy; uses deploy token)
The two pieces are:
-
Server side: set
SHINYHUB_DEPLOY_TOKEN(at least 32 characters) on the server process. Optionally setSHINYHUB_DEPLOY_TOKEN_ROLEto control what the configuration-managed deployment credential can do (default:developer), andSHINYHUB_DEPLOY_TOKEN_APPS(comma-separated slugs) to restrict the token to specific apps. -
Client side: set
SHINYHUB_HOSTto the direct URL of the ShinyHub port (not the proxied hostname) andSHINYHUB_TOKENto the same value you set forSHINYHUB_DEPLOY_TOKEN.
Server configuration¶
In the environment of the shinyhub serve process:
SHINYHUB_DEPLOY_TOKEN="$(openssl rand -hex 32)" # generate once; store in your secrets manager
SHINYHUB_DEPLOY_TOKEN_ROLE=developer # optional; default is developer
SHINYHUB_DEPLOY_TOKEN_APPS=sales,hr-dashboard # optional; restrict the token to these apps
ShinyHub persists only the token's SHA-256 hash and non-secret metadata under
the built-in Deployment automation service account. The elected control-plane
owner applies this configuration. On one server, change the variable and restart
to rotate it; removing the variable and restarting revokes the credential. In HA,
update every replica first and then perform the normal ownership handover/rolling
restart. A standby never overwrites the shared credential with stale rollout
configuration. The compatibility username remains
__deploy__, but it is not an interactive user and cannot sign in.
For multiple teams, prefer a separate scoped credential per team or pipeline:
shinyhub service-accounts credentials create deployment \
--name "analytics production CI" --role developer \
--app sales --app forecasting --expires-in-days 90
Human users are not displaced by the service account. A person with the
developer, operator, or admin role can still deploy with their own session
or personal token; those deployments remain attributed to that person.
Scope the token to what CI actually deploys. With
SHINYHUB_DEPLOY_TOKEN_APPS set, the token can only see, deploy, and manage
the listed slugs (it may create them if they do not exist yet); every other
app returns 404 on app-specific surfaces, regardless of the token's role. The
allowlist is also the explicit grant for private apps, so a team credential
does not need shared ownership or membership rows. Scoped credentials cannot
change the shared project catalog or read the global audit log; pre-provision
project metadata with a human admin or an unrestricted operator credential.
This caps the blast radius of a leaked CI secret.
App scope does not turn an admin credential into a scoped administrator:
people and server-setting authority remains platform-wide. Avoid
SHINYHUB_DEPLOY_TOKEN_ROLE=admin unless the pipeline genuinely manages the
platform; the server and credential-creation UI warn about that distinction.
Worked example¶
Assume ShinyHub listens on 10.0.1.5:8080 (reachable on your LAN or VPN), and
the proxied public hostname is https://shiny.example.com.
Deploy an app:
SHINYHUB_HOST=http://10.0.1.5:8080 \
SHINYHUB_TOKEN=your-deploy-token-here \
shinyhub deploy ./my-app --slug myapp
Set an environment variable:
SHINYHUB_HOST=http://10.0.1.5:8080 \
SHINYHUB_TOKEN=your-deploy-token-here \
shinyhub env set myapp KEY=value
The two env vars can also be written to the CLI credentials file
(shinyhub connect http://10.0.1.5:8080), but for CI pipelines injecting
them as environment variables is simpler and avoids storing credentials on disk.
CI pipeline example¶
# GitHub Actions excerpt
- name: Deploy to ShinyHub
env:
SHINYHUB_HOST: http://10.0.1.5:8080
SHINYHUB_TOKEN: ${{ secrets.SHINYHUB_DEPLOY_TOKEN }}
run: shinyhub deploy . --slug myapp --wait
Security notes¶
Restrict direct access to the app port. If SHINYHUB_DEPLOY_TOKEN is set,
anyone who can reach the app port and knows the token can deploy apps or change
settings. Restrict reachability to operators: bind the app port to a private
network interface, or protect it behind a firewall or VPN so only the proxy (for
browser traffic) and authorised operators (for CLI/CI) can connect.
trusted_proxies and the direct CLI path. server.trusted_proxies
controls which peer IP addresses may inject forward-auth identity headers
(X-Forwarded-User, etc.). A direct CLI connection from an address that is NOT
in trusted_proxies is treated as an ordinary authenticated client: the deploy
token is validated, but no forward-auth identity is accepted. This is the
correct behaviour - the CLI is not acting as a proxy and should not be able to
claim an arbitrary identity.
App visibility and the app port. When defaults.app_visibility is set to
public and the app port is directly reachable (LAN, VPN, or any other
interface), apps are accessible with no ShinyHub-level authentication from that
path. See the note in the configuration file and in the App visibility and auth
proxy section below for how to handle this.
App visibility and the auth proxy¶
A common pattern behind an auth proxy is to set defaults.app_visibility:
public so the proxy handles all authentication and ShinyHub does not add a
second login prompt. This is safe only when the auth proxy is the EXCLUSIVE
ingress to ShinyHub.
If the ShinyHub app port is also reachable directly (LAN, VPN, Tailscale, or
a second network interface), public means apps are accessible with no auth at
all from that path - the proxy is bypassed and ShinyHub grants access freely.
In that case, prefer one of these approaches:
-
Set
defaults.app_visibility: private. Users who reach ShinyHub through the auth proxy still get a valid identity (forward-auth), so they are authenticated and see apps they have access to. Direct CLI clients use their own personal or scoped service-account credential. No app is exposed without auth. -
Keep
publicand firewall the app port so only the auth proxy host can reach it. Direct CLI access then goes through the proxy or via a side-channel (such as an SSH tunnel or a separate internal-only port).
Config file vs. credentials file¶
ShinyHub has two distinct config concepts:
-
Server config file (
shinyhub.yaml): containsauth,server,storage, and other server settings. Selected withshinyhub serve --config <file>or theSHINYHUB_CONFIGenv var on the SERVER process. -
Client credentials file (
~/.config/shinyhub/config.json): stores the host URL and API token written byshinyhub connectorshinyhub login. Selected with the--configflag on CLIENT commands, or theSHINYHUB_CONFIGenv var on the CLIENT side. When usingSHINYHUB_HOSTandSHINYHUB_TOKENdirectly, the credentials file is not read.
The SHINYHUB_CONFIG name is shared by both roles (server config path on the
server, client credentials path on the client), which can be confusing. When
operating a server, set SHINYHUB_CONFIG in the server process environment. In
a CI job that runs only client commands, SHINYHUB_CONFIG points to the
credentials file - but for the token-based flow described in this guide,
SHINYHUB_HOST and SHINYHUB_TOKEN are simpler and do not require a file.