Secure the studio
You will take a studio from a development setup to one you can deploy: strong credentials from the environment, JWT auth that works across isolates, HTTPS cookies behind a proxy, and an extra network gate. The result is an admin that only your team can reach and sign in to.
Prerequisites
Section titled “Prerequisites”- A working studio (see Add an admin to an app).
- Read Security model for what the studio does and does not protect.
1. Remove development overrides
Section titled “1. Remove development overrides”Make sure neither of these reaches production:
GisilaStudio(db: db, allowUnauthenticated: true); // no login at allGisilaStudio(db: db, allowInsecure: true); // accepts weak credentialsand that GISILA_ALLOW_INSECURE=1 is not set in the production environment.
2. Use strong credentials from the environment
Section titled “2. Use strong credentials from the environment”STUDIO_USERNAME=ops-adminSTUDIO_PASSWORD="$(openssl rand -base64 24)" # 12+ characters, not the usernameSTUDIO_SECRET="$(openssl rand -base64 48)" # 32+ charactersStore them in your secret manager or deployment platform, not in the repository. With all three
set, GisilaStudio(db: db) builds a JwtStudioAuth.
If the credentials are too weak, the studio does not start serving: it returns 503 and logs,
for example, gisila_studio: "Shop Admin" is disabled: its login is too weak: the password is shorter than 12 characters ....
3. Prefer JWT auth in production
Section titled “3. Prefer JWT auth in production”SessionStudioAuth keeps sessions in one isolate’s memory. If you start the server with
shared: true on several isolates, or run more than one instance, sign-in appears to work and
then fails at random. JwtStudioAuth validates tokens with the shared secret instead.
Set the token lifetime explicitly; logout does not revoke a JWT, so a short lifetime limits a leaked token:
import 'dart:io';
import 'package:gisila_studio/gisila_studio.dart';
final env = Platform.environment;final studio = GisilaStudio( db: db, title: 'Shop Admin', auth: JwtStudioAuth( username: env['STUDIO_USERNAME']!, password: env['STUDIO_PASSWORD']!, secret: env['STUDIO_SECRET']!, expiry: const Duration(hours: 2), ),);4. Serve over HTTPS
Section titled “4. Serve over HTTPS”The cookie gets the Secure flag when the request is HTTPS, or when a proxy sends
X-Forwarded-Proto: https. Terminate TLS at your proxy and make sure it sets that header (most
do by default). Without HTTPS, credentials and the session cookie travel in clear text.
5. Restrict who can reach it
Section titled “5. Restrict who can reach it”The login is the only gate inside the studio, and sign-in throttling is per client address. Add a network-level restriction as well, for example an IP allowlist in front of the mount:
import 'dart:io';
import 'package:shelf/shelf.dart';
Middleware allowOnly(Set<String> addresses) => (inner) => (Request req) { final info = req.context['shelf.io.connection_info']; final ip = info is HttpConnectionInfo ? info.remoteAddress.address : null; if (ip == null || !addresses.contains(ip)) return Response.forbidden('Forbidden'); return inner(req); };
router.mount( '/admin', const Pipeline() .addMiddleware(allowOnly({'10.0.0.5', '10.0.0.6'})) .addHandler(studio.handler(prefix: '/admin')),);Behind a reverse proxy the TCP peer is the proxy, so do this filtering at the proxy instead (or in a VPN / private network). The same caveat applies to sign-in throttling: behind a proxy all clients share one counter, and ten failed attempts lock everyone out for 15 minutes.
6. Protect secret columns
Section titled “6. Protect secret columns”For each table with password or token columns:
studio.register<User>( UserTable.metadata, onSave: {'password': (v) => hashPassword(v as String)},);or make the column read-only. register() warns at startup about editable password columns
with no hook. Check passwordFields covers every secret column (the default only matches names
containing password, passwd or secret; an api_token column needs adding).
7. Use a least-privilege database role (optional)
Section titled “7. Use a least-privilege database role (optional)”The studio runs SQL with whatever role the Database connects as, including DELETE. Give it
a separate gisila_orm connection whose role can only touch the registered tables.
Verify it works
Section titled “Verify it works”# Unauthenticated requests are redirected to the login pagecurl -si https://admin.example.com/admin/ | grep -i '^location'# A cookie set over HTTPS carries Securecurl -si https://admin.example.com/admin/login/ \ -d 'username=ops-admin' --data-urlencode "password=$STUDIO_PASSWORD" | grep -i set-cookie# set-cookie: _studio_jwt=...; Path=/admin; HttpOnly; SameSite=Lax; Secure
# Cross-site POSTs without the CSRF token are refusedcurl -si -X POST https://admin.example.com/admin/product/1/delete/ \ -H "cookie: _studio_jwt=$TOKEN" | head -1# HTTP/1.1 403 Forbidden