Skip to content

Admin Console

Ingress can embed an operations console in the same process as the reverse proxy. Enable it with the top-level admin: block in ingress.yaml — there is no separate ingress admin subcommand.

The console exposes an HTTP API (default port 9080) and a React UI for routes, logs, TLS, cache, WAF events, and config editing with validate / publish / reload.

Quick start

Minimal config:

yaml
version: v1
port: 8080

admin:
  enabled: true
  port: 9080

rules:
  - host: example.com
    backend:
      service:
        name: backend-service
        port: 8080

Run:

bash
ingress run -c ingress.yaml

Startup logs include:

text
Admin started at http://127.0.0.1:9080
Server started at http://127.0.0.1:8080

Open http://127.0.0.1:9080 for the built-in UI (when admin.web.dev_proxy is false, the default in production builds).

By default admin.auth.type is none (no login gate). Set basic or oauth before exposing the admin port on untrusted networks — see Authentication & RBAC.

Full demo bundle: examples/admin-console/ — multi-route sample, sample logs, TLS certs, and SQLite state. See the Admin console example.

Configuration

FieldTypeDescriptionDefault
admin.enabledboolStart the admin server with ingress runfalse
admin.portintAdmin listen port9080
admin.database.driverstringAudit / revision DB driversqlite
admin.database.dsnstringSQLite DSN (relative paths resolve beside the ingress config file)file:admin.db?cache=shared&_fk=1
admin.web.dev_proxyboolAPI only; run the UI from Vite dev server (proxies /api)false
admin.auth.typestringConsole login mode: none (default), basic, or oauthnone
admin.auth.basic.usernamestringBootstrap super-admin RBAC username (synced on every startup)admin when password set; else internal default
admin.auth.basic.passwordstringPassword for the bootstrap user (RBAC bcrypt hash; only applied on first create)admin when username set; else internal default
admin.auth.oauth.*objectThird-party OAuth (provider, client_id, client_secret, optional redirect_url, scopes)
admin.access_log_pathstringOverride access log path for the log viewerfrom logging file transport
admin.error_log_pathstringOverride error log path for the log viewerfrom logging file transport

Example with local SQLite and UI dev mode:

yaml
version: v1
port: 8080
admin:
  enabled: true
  port: 9080
  database:
    driver: sqlite
    dsn: file:./admin.db?cache=shared&_fk=1
  web:
    dev_proxy: true
  geoip:
    ingress_label: 上海
    ingress_lat: 31.2304
    ingress_lng: 121.4737
cache:
  ttl: 300
  prefix: "ingress:"
waf:
  enabled: true
  log_only: false
  builtin: true
  trust_proxy: true
healthcheck:
  outer:
    enable: true
    path: /healthz
    ok: true
  inner:
    enable: true
    interval: 30
    timeout: 5
https:
  port: 8443
  redirect_from_http:
    permanent: true
  ssl:
    - domain: api.example.com
      cert:
        certificate: ./certs/api.example.com.pem
        certificate_key: ./certs/api.example.com.key.pem
    - domain: cdn.example.com
      cert:
        certificate: ./certs/cdn.example.com.pem
        certificate_key: ./certs/cdn.example.com.key.pem
    - domain: assets.cdn.example.com
      cert:
        certificate: ./certs/assets.cdn.example.com.pem
        certificate_key: ./certs/assets.cdn.example.com.key.pem
    - domain: admin.internal
      cert:
        certificate: ./certs/admin.internal.pem
        certificate_key: ./certs/admin.internal.key.pem
    - domain: legacy.example.com
      cert:
        certificate: ./certs/legacy.example.com.pem
        certificate_key: ./certs/legacy.example.com.key.pem
    - domain: tunnel-a.inlets.example.com
      cert:
        certificate: ./certs/tunnel-a.inlets.example.com.pem
        certificate_key: ./certs/tunnel-a.inlets.example.com.key.pem
    - domain: waf-demo.example.com
      cert:
        certificate: ./certs/waf-demo.example.com.pem
        certificate_key: ./certs/waf-demo.example.com.key.pem
    - domain: portal.example.com
      cert:
        certificate: ./certs/portal.example.com.pem
        certificate_key: ./certs/portal.example.com.key.pem
fallback:
  type: handler
  handler:
    type: static_response
    headers:
      Content-Type: text/plain; charset=utf-8
    body: |
      fallback ok
services:
  - name: api.internal
    port: 8080
    note: API 主集群(演示用 handler 替代)
  - name: api-v2.internal
    port: 8080
    note: API v2 路径专用
  - name: home
    port: 8080
rules:
  - host: api.example.com
    backend:
      type: handler
      handler:
        type: static_response
        headers:
          Content-Type: application/json
        body: |
          {"ok":true,"service":"api"}
      cache:
        enabled: true
        ttl: 300
    paths:
      - path: /v2
        backend:
          type: handler
          handler:
            type: static_response
            headers:
              Content-Type: application/json
            body: |
              {"v2":true,"users":[]}
          cache:
            enabled: true
            ttl: 600
            max_body_bytes: 2097152
      - path: /public
        backend:
          type: handler
          handler:
            type: static_response
            headers:
              Content-Type: application/json
            body: |
              {"public":true}
          cache:
            enabled: true
            ttl: 300
            max_body_bytes: 2097152
      - path: /search
        backend:
          type: handler
          handler:
            type: static_response
            headers:
              Content-Type: application/json
            body: |
              {"results":[]}
      - path: /error/400
        backend:
          type: handler
          handler:
            type: static_response
            status_code: 400
            headers:
              Content-Type: application/json
            body: |
              {"error":"bad request"}
      - path: /error/403
        backend:
          type: handler
          handler:
            type: static_response
            status_code: 403
            headers:
              Content-Type: application/json
            body: |
              {"error":"forbidden"}
      - path: /error/500
        backend:
          type: handler
          handler:
            type: static_response
            status_code: 500
            headers:
              Content-Type: application/json
            body: |
              {"error":"internal"}
  - host: cdn.example.com
    backend:
      type: handler
      handler:
        type: file_server
        root_dir: ./static
        index_file: assets/app.js
      cache:
        enabled: true
        ttl: 3600
  - host: assets.cdn.example.com
    host_type: exact
    backend:
      type: handler
      handler:
        type: file_server
        root_dir: ./static
        index_file: static/main.js
      cache:
        enabled: true
        ttl: 3600
  - host: portal.example.com
    backend:
      type: handler
      handler:
        type: static_response
        headers:
          Content-Type: text/html; charset=utf-8
        body: |
          <!doctype html><html><body><h1>portal</h1></body></html>
      cache:
        enabled: true
        ttl: 120
  - host: ^([a-z0-9-]+)\.inlets\.example\.com$
    host_type: regex
    backend:
      service:
        name: ${host.1}.tunnel
        port: 443
        protocol: https
  - host: admin.internal
    backend:
      type: handler
      handler:
        headers:
          Content-Type: text/plain; charset=utf-8
        body: |
          admin console demo host
      cache:
        enabled: true
        ttl: 60
    paths:
      - path: /healthz
        backend:
          type: handler
          handler:
            headers:
              Content-Type: text/plain; charset=utf-8
            body: |
              ok
  - host: legacy.example.com
    backend:
      type: redirect
      redirect:
        url: https://www.example.com$request_uri
        permanent: true
      cache:
        enabled: true
        ttl: 120
  - host: httpbin.work
    backend:
      service:
        mode: external
        protocol: https
        name: httpbin.zcorky.com
        port: 443
scenarios:
  active: default
  items:
    - id: peak
      label: 高峰
      description: Admin 演示 — 延长 api.example.com 缓存 TTL
      overlay:
        rules:
          - host: api.example.com
            backend:
              cache:
                enabled: true
                ttl: 900

Only the admin: stanza is required for the console; the rest of the file is a routing demo.

Logging and the log viewer

The admin Logs page reads from file paths on disk. By default it uses the same paths as ingress logging (after prepare/normalize).

When admin.enabled: true and logging is unset (no enable, level, or transports), ingress defaults to:

  • logging.enable: true
  • File transport beside the config file: access.log and error.log

Explicit logging.* always wins — including logging.enable: false or custom transports. When admin is disabled, unset logging still defaults to /var/log/ingress/access.log and error.log if you set logging.enable: true without transports.

Override only for the admin reader (without changing ingress logging):

yaml
admin:
  enabled: true
  access_log_path: /var/log/ingress/access.log
  error_log_path: /var/log/ingress/error.log

Access log line format matches Configuration · Access log fields. Query filters include cache_hit, waf_block, host, status, and byte offset for tailing.

UI development

For frontend work, enable dev proxy and run Vite separately:

yaml
admin:
  enabled: true
  web:
    dev_proxy: true
bash
ingress run -c ingress.yaml
cd core/admin/web && pnpm dev

The Vite dev server proxies /api to the admin port. Production UI is embedded at compile time after cd core/admin && make build (-tags adminui; output under core/admin/static/dist, not committed to git).

HTTP API

Base path: /api/v1. Responses use JSON envelopes from the admin handler layer.

MethodPathPurpose
GET/statusProcess / config summary
GET/routesFlattened route table
POST/routes/matchDry-run match (host, path JSON body)
GET/logsTail/search access or error logs
GET/metrics/overviewAggregates from in-process rollup (falls back to access log tail)
GET/waf/eventsRecent WAF audit rows (SQLite)
GET/tls/certsCertificate metadata from config paths
POST/tls/certs/checkInspect one domain
GET/cache/overviewCache engine / key overview
GET/configRead ingress YAML
PUT/configSave YAML (revision recorded)
POST/config/validateValidate YAML body or on-disk file
POST/config/previewDiff / preview pending changes
POST/config/publishValidate, save, and reload
POST/config/modulesList config editor modules
POST/config/modules/mergeMerge one module patch
GET/config/revisionsList saved revisions
GET/config/revisions/:idOne revision
POST/reloadValidate on-disk config and reload ingress
GET/auth/configLogin mode and current session user
POST/auth/loginBasic login (username, password JSON)
POST/auth/logoutClear session
GET/auth/oauth/loginStart OAuth redirect (?redirect= optional)
GET/auth/oauth/callbackOAuth callback (handled by provider redirect)
GET/rbac/menusSidebar tree filtered by the current user's menu:* grants
GET/POST/PUT/DELETE/rbac/users, /rbac/roles, /rbac/permissionsRBAC management
GET/routes/:ri/:piRoute detail (config + auth/cache/healthcheck)
GET/routes/:ri/:pi/metricsRoute-level aggregated metrics
GET/events/streamSSE real-time event stream (?channels=...)
GET/healthcheckHealth check probe results and summary
GET/settingsAdmin + ingress settings snapshot
GET/jobsScheduled jobs (built-in + custom)
POST/jobs/:source/:id/runRun one job immediately

Scheduled jobs (cron, jobs: in YAML, job_run history): see Scheduled jobs.

Reload from the console validates the config file, then triggers an in-process reload (same outcome as ingress reload / SIGHUP when started with ingress run).

Real-time events (SSE)

The admin console pushes real-time updates over Server-Sent Events (SSE). Connect to:

GET /api/v1/events/stream?channels=metrics,waf,logs,health

Supported channels: metrics, waf, logs, health. The UI auto-subscribes based on the current page. Up to 5 concurrent SSE connections per IP are allowed; the client automatically falls back to polling if the limit is reached or SSE is unavailable.

Overview metrics data path

GET /api/v1/metrics/overview?window=15m returns JSON with a source field describing how the window was built:

sourceMeaning
rollup_liveIn-process ring buffer only (typical when ingress runs with Admin and live traffic)
rollup_hybridSQLite minute buckets for older minutes + live buffer for recent data
rollup_persistedSQLite minute buckets only (e.g. long window with empty live buffer after restart)
access_logParsed access log tail (fallback when rollup does not cover the window)
access_log_partialTail read hit line cap before window start
errorLog read/parse failure

Live path: each proxied request calls logAccess() in ingress core, which emits AccessMetricsEvent into Admin MetricsRollup.Record. Cold start: Admin loads persisted buckets (26h), seeds up to 1h from the access log when the buffer is empty, then tails new log lines only when Admin runs without CoreInstance (avoids double-counting). Persistence: closed minute aggregates flush to SQLite every minute; built-in job purge_metrics_buckets prunes buckets older than params.retain_days (default 30).

Logs for the Logs page still use SSE tail + offset; only overview aggregates use rollup.

Overview timeline chart linking

Time-series panels on the same page (per-host traffic, site-wide traffic, quality, cache, upstream latency, …) use ECharts connect: hovering one chart highlights the same time bucket on the others. Category labels come from the same metrics.timeline[] (host_timeline[].points[].label is aligned). Non-time panels (status donut, latency SLO share, Top-N lists, …) are not linked.

Event format:

event: channel:action
data: {"key": "value", ...}

Route detail

Click any route row to open the route detail page at /routes/:ruleIndex/:pathIndex. It shows:

  • Full route configuration (host, path, backend, auth, cache, health check, WAF)
  • Real-time metrics (QPS, latency percentiles, error rate, cache hit ratio)
  • Filtered logs, WAF events, and cache data for that specific route

Topology graph

The Topology page (/topology) renders a three-layer SVG diagram: Host → Path → Backend. Node colors indicate health status (green = up, yellow = warning, red = down). Click a node to navigate to the route detail or routes list.

Health check panel

The Health page (/health) displays the status of all backends that have healthcheck configured. The backend probes every 30 seconds with a 5-second timeout. State changes are pushed via the SSE health channel.

Config draft & undo

The config editor tracks edit history (up to 50 steps). Use Ctrl+Z / Cmd+Z to undo, Ctrl+Shift+Z / Cmd+Shift+Z to redo. The draft badge in the tab bar shows unsaved changes.

One-click rollback

In the config version history panel, each revision has a Rollback button. Clicking it opens a confirmation dialog, then validates, publishes, and reloads the selected version — no manual copy-paste needed.

Certificate expiry alerts

The Overview page now reads real TLS certificate data instead of hard-coded values. Certificates expiring within 30 days show a yellow warning; within 7 days, a red critical alert.

Version consistency badge

The Overview page compares the running config hash with the latest revision hash. A green badge means "config consistent"; yellow means "changes not yet published".

Authentication & RBAC

Admin Console login is separate from route-level backend.service.auth (Basic/Bearer on proxied upstreams). Configure it under admin.auth.

Minimal basic login:

yaml
version: v1
port: 8080

# Minimal Admin Console with local login + RBAC.
# Default UI login: admin / admin (see admin.auth.basic below).
admin:
  enabled: true
  port: 9080
  database:
    driver: sqlite
    dsn: file:./admin-auth.db?cache=shared&_fk=1
  auth:
    type: basic
    basic:
      username: admin
      password: admin

rules:
  - host: app.example.com
    backend:
      service:
        name: app-service
        port: 8080

Focused examples: examples/admin-auth/ — see the Admin auth example.

Login modes

admin.auth.typeBehavior
none (default)No login gate — suitable for localhost / trusted networks only
basicLocal login page; credentials validated against RBAC users in SQLite
oauthRedirect to a supported provider (GitHub, GitLab, Google, Feishu, …)

When admin.auth.type is basic or oauth, all /api/v1/* routes except the auth endpoints require a valid session cookie.

Bootstrap super-admin

On every startup, ingress syncs the user named in admin.auth.basic.username (default admin) into RBAC:

  • Creates the user on first run (password from admin.auth.basic.password, default admin when omitted)
  • Ensures the user always has the builtin admin role (super-admin)
  • Password changes in the UI are kept; config password is not overwritten on later restarts

Manage additional operators under 权限 in the sidebar (users, roles, permissions).

RBAC model

EntityPurpose
PermissionsFine-grained grants — action codes (routes:read, config:write, …) and menu:* sidebar visibility
RolesNamed permission sets assigned to users
UsersConsole operator accounts (bcrypt password hashes)

Menu vs action permissions

  • Sidebar items require matching menu:* grants (for example menu:routes for the Routes link)
  • Action grants alone (for example routes:read without menu:routes) do not show a menu entry
  • When editing roles, enable both action and 菜单 groups as needed

Login gate

  • Basic login succeeds only when the account has at least one visible menu
  • Otherwise the API returns 403 and no session is created (password-only roles without menus cannot sign in)

Builtin roles

Seeded on startup (builtin roles cannot be deleted):

CodeNameTypical use
admin管理员Full console access
viewer只读观察Read-only monitoring, traffic, security pages
operator运维工程师Viewer + maintenance, jobs, Web terminal
developer路由开发Routes, services, cache, config, settings
security安全工程师Events, logs, WAF, TLS, health checks

OAuth (optional)

yaml
admin:
  auth:
    type: oauth
    oauth:
      provider: github
      client_id: "..."
      client_secret: "..."
      # redirect_url: https://admin.example.com/api/v1/auth/oauth/callback
      scopes:
        - user:email

After OAuth, the session username is derived from the provider profile. Assign matching RBAC users/roles if you need menu filtering beyond the OAuth identity.

Security notes

  • admin.auth.type defaults to none (open API/UI). Enable basic or oauth before exposing the admin port beyond localhost or a trusted network.
  • Config publish writes the live ingress.yaml and reloads the proxy — restrict who can reach the admin port even when auth is enabled.
  • Do not expose admin.web.dev_proxy: true on untrusted networks.

Validate before run or deploy:

bash
ingress validate -c ingress.yaml

Reload after editing the file on disk:

bash
ingress reload -c ingress.yaml
# or: kill -HUP $(cat /tmp/gozoox.ingress.pid)

See Getting started · Command line options for run, validate, and reload flags.

Released under the MIT License.