A3S Docs
A3S Gateway

Configuration

Complete ACL reference for entrypoints, routers, services, middlewares, observability, management, and providers

Configuration

A3S Gateway is configured exclusively in ACL (Agent Configuration Language). This page is the complete reference for the configuration schema in v1.0.11: every block, every key, its type, and its default.

Configuration files must use the .acl extension. The loader rejects .toml, .yaml, .yml, and .json files. Configuration supports hot reload via file watching (see File Provider).

Top-level structure

A configuration file is composed of named blocks plus one scalar. The parser accepts singular or plural spellings for the four core blocks (entrypoint/entrypoints, router/routers, service/services, middleware/middlewares). Any other top-level block is a hard error (Unknown top-level ACL block).

pub struct GatewayConfig {
    pub entrypoints: HashMap<String, EntrypointConfig>,
    pub routers: HashMap<String, RouterConfig>,
    pub services: HashMap<String, ServiceConfig>,
    pub middlewares: HashMap<String, MiddlewareConfig>,
    pub providers: ProviderConfig,
    pub management: ManagementConfig,
    pub observability: ObservabilityConfig,
    pub shutdown_timeout_secs: u64, // default: 30
}

Each block instance is named either with a string label (routers "api" { ... }) or a name attribute (routers { name = "api" ... }).

Top-level blockPurpose
entrypointsNetwork listeners (HTTP / TCP / UDP)
routersMatch requests to a service + middleware chain
servicesBackend pools, load balancing, scaling, traffic splitting
middlewaresReusable request/response processors
observabilityMetrics, access log, tracing toggles
managementDedicated dashboard / admin API listener
providersDynamic config sources (file / discovery / kubernetes / docker)
shutdown_timeout_secsGraceful shutdown timeout in seconds (default 30)

shutdown_timeout_secs is a top-level block (not a bare scalar) in ACL:

shutdown_timeout_secs {
  shutdown_timeout_secs = 30
}

Loading configuration

// Load from a .acl file
let config = GatewayConfig::from_file("gateway.acl").await?;

// Parse from an ACL string
let config = GatewayConfig::from_acl(acl_content)?;

// Validate cross-references and value constraints
config.validate()?;

validate() cross-checks every routers.service / routers.middlewares / routers.entrypoints reference, requires each service to declare at least one server or at least one revision, parses and validates request_timeout, validates scaling/revision/rollout constraints, and — when management.enabled — validates the management address, path prefix, allowlist, and TLS.

Environment variable substitution

Any string-valued attribute may read from the environment with env("VAR"). A missing variable is a configuration error. env(...) is the only function call supported in values; any other Call() form is an error.

middlewares "auth-jwt" {
  type  = "jwt"
  value = env("JWT_SECRET")
}

entrypoints "websecure" {
  address = "0.0.0.0:443"
  tls {
    cert_file = env("TLS_CERT_PATH")
    key_file  = env("TLS_KEY_PATH")
  }
}

Entrypoints

Entrypoints define network listeners. The protocol selects the listener type; address is required.

pub struct EntrypointConfig {
    pub address: String,                       // required, "host:port"
    pub protocol: Protocol,                    // http (default) | tcp | udp
    pub tls: Option<TlsConfig>,                // optional
    pub max_connections: Option<u32>,          // TCP only, default: unlimited
    pub tcp_allowed_ips: Vec<String>,          // TCP only, default: []
    pub udp_session_timeout_secs: Option<u64>, // UDP only, default: 30 at the listener
    pub udp_max_sessions: Option<usize>,       // UDP only, default: 10000 at the listener
}
KeyTypeDefaultNotes
addressStringrequiredhost:port; parsed as a socket address.
protocolhttp | tcp | udphttpAny other value is rejected (Invalid entrypoint protocol).
tlsblocknoneTLS termination (HTTP entrypoints).
max_connectionsu32unlimitedTCP only; enforced via a connection semaphore.
tcp_allowed_ips[String][] (allow all)TCP only; CIDR or single IP.
udp_session_timeout_secsu6430UDP only. When unset, 30 is applied at the listener.
udp_max_sessionsusize10000UDP only. When unset, 10000 is applied at the listener.

The UDP defaults (30 / 10000) are not schema defaults — the fields are optional and the values are applied by the UDP listener when omitted.

# HTTP listener
entrypoints "web" {
  address = "0.0.0.0:80"
}

# HTTPS listener with static TLS
entrypoints "websecure" {
  address = "0.0.0.0:443"
  tls {
    cert_file   = "/etc/certs/cert.pem"
    key_file    = "/etc/certs/key.pem"
    min_version = "1.3"
  }
}

# TCP listener with connection limit + IP allowlist
entrypoints "postgres" {
  address         = "0.0.0.0:5432"
  protocol        = "tcp"
  max_connections = 1000
  tcp_allowed_ips = ["10.0.0.0/8", "192.168.1.0/24"]
}

# UDP listener
entrypoints "dns" {
  address                  = "0.0.0.0:53"
  protocol                 = "udp"
  udp_session_timeout_secs = 60
  udp_max_sessions         = 5000
}

Entrypoint TLS

pub struct TlsConfig {
    pub cert_file: String,                 // PEM cert chain
    pub key_file: String,                  // PEM private key
    pub min_version: String,               // "1.2" (default) or "1.3"
    pub acme: bool,                        // default: false
    pub acme_email: Option<String>,        // required when acme = true
    pub acme_domains: Vec<String>,         // default: [] (derives from Host rules)
    pub acme_staging: bool,                // default: false
    pub acme_storage_path: Option<String>, // default: /etc/gateway/acme
}
KeyTypeDefaultNotes
cert_fileString""PEM cert chain. Required in practice for static certs.
key_fileString""PEM private key. Required in practice.
min_versionString"1.2""1.2" enables TLS 1.2 and 1.3; "1.3" is 1.3-only. Entrypoint TLS does not validate this string at load time.
acmeboolfalseEnable the ACME (Let's Encrypt) manager.
acme_emailStringnoneRequired when acme = true; if empty the ACME manager is skipped with a warning.
acme_domains[String][]When empty, the gateway derives domains from Host(`...`) router rules.
acme_stagingboolfalseUse the Let's Encrypt staging directory.
acme_storage_pathString/etc/gateway/acmeCertificate/account storage directory.

TLS termination is rustls-based (pure Rust, no OpenSSL). min_version is a minimum: "1.2" still allows TLS 1.3.

entrypoints "websecure" {
  address = "0.0.0.0:443"
  tls {
    cert_file         = "/etc/certs/cert.pem"
    key_file          = "/etc/certs/key.pem"
    acme              = true
    acme_email        = "admin@example.com"
    acme_domains      = ["example.com", "api.example.com"]
    acme_staging      = false
    acme_storage_path = "/etc/gateway/acme"
  }
}

ACME from configuration is HTTP-01 only in v1.0.11. The tls block exposes only the keys above. There are no DNS-01 / Cloudflare / Route53 keys reachable from ACL — those solvers exist in the runtime but are not wired to the configuration schema. The challenge type is always HTTP-01 and the DNS provider is always None.

Routers

Routers match incoming requests to a service and an ordered middleware chain.

pub struct RouterConfig {
    pub rule: String,             // required, Traefik-style matcher
    pub service: String,          // required, must exist
    pub entrypoints: Vec<String>, // default: [] (= all entrypoints)
    pub middlewares: Vec<String>, // default: []
    pub priority: i32,            // default: 0
}
KeyTypeDefaultNotes
ruleStringrequiredMatcher expression, e.g. Host(`d`), PathPrefix(`/p`), Path(`/x`), Headers(`k`,`v`), Method(`GET`), joined with &&.
serviceStringrequiredTarget service; must exist in services.
entrypoints[String][]Listeners this router binds to. Empty = all entrypoints. Each name must exist.
middlewares[String][]Ordered chain; each name must exist.
priorityi320Higher positive value wins (most-specific). See note below.
routers "api" {
  rule        = "Host(`api.example.com`) && PathPrefix(`/v1`)"
  service     = "api-service"
  entrypoints = ["websecure"]
  middlewares = ["auth-jwt", "rate-limit"]
  priority    = 100
}

routers "web" {
  rule        = "Host(`www.example.com`)"
  service     = "web-service"
  entrypoints = ["web", "websecure"]
}

Router priority is "higher wins". When priority > 0 the value is used directly and the highest value is the most specific (Traefik-style). When priority <= 0 (the default) the gateway falls back to the rule-string length (longer rule wins). Setting a negative priority does not raise precedence — it is ignored. (TCP routes, which are generated only by providers, use the opposite convention — lower wins.) See Routing for the full matcher grammar and ordering rules.

Services

A service wraps a load balancer plus optional scaling, revision splitting, rollout, mirroring, and failover.

pub struct ServiceConfig {
    pub load_balancer: LoadBalancerConfig,
    pub scaling: Option<ScalingConfig>,
    pub revisions: Vec<RevisionConfig>,
    pub rollout: Option<RolloutConfig>,
    pub mirror: Option<MirrorConfig>,
    pub failover: Option<FailoverConfig>,
}

A service must declare at least one server or at least one revision, otherwise validate() fails with no servers configured. Omitting the load_balancer block yields an empty round-robin / 30s pool that triggers this error unless revisions provide backends.

Load balancer

pub struct LoadBalancerConfig {
    pub strategy: Strategy,                  // round-robin (default) | weighted | least-connections | random
    pub request_timeout: String,             // default: "30s"
    pub servers: Vec<ServerConfig>,          // default: []
    pub health_check: Option<HealthCheckConfig>,
    pub sticky: Option<StickyConfig>,
}
KeyTypeDefaultNotes
strategyround-robin | weighted | least-connections | randomround-robinUnknown values are rejected (unknown strategy).
request_timeoutduration String"30s"Max wait for an upstream response; on elapse the request returns HTTP 504.
serverslist[]Backend servers.
health_checkblocknoneActive HTTP health check.
stickyblocknoneCookie-based session affinity.

request_timeout (and the health-check durations) use a custom parser: suffixes ms, s, m; a bare number means seconds. Empty, 0s, or non-numeric values are rejected.

services "api-service" {
  load_balancer {
    strategy        = "weighted"
    request_timeout = "30s"
    servers = [
      { url = "http://127.0.0.1:8001", weight = 3 },
      { url = "http://127.0.0.1:8002", weight = 1 }
    ]

    health_check {
      path                = "/health"
      interval            = "10s"
      timeout             = "5s"
      unhealthy_threshold = 3
      healthy_threshold   = 1
    }

    sticky {
      cookie = "srv_id"
    }
  }
}

Servers

KeyTypeDefaultNotes
urlStringrequirede.g. http://127.0.0.1:8001, or h2c://host:port for cleartext gRPC.
weightu321Used by the weighted strategy only.

Servers accept three spellings: an inline list servers = [ { ... } ], a single object, or repeated server / servers child blocks.

Health check

Presence of the health_check block starts the active checker for that service.

KeyTypeDefaultNotes
pathStringrequiredHTTP path to probe, e.g. /health.
intervalduration String"10s"Time between probe rounds.
timeoutduration String"5s"Per-probe timeout.
unhealthy_thresholdu323Consecutive failures before marking unhealthy.
healthy_thresholdu321Consecutive successes before marking healthy.

Any non-2xx response (or request error) counts as a failure. A separate passive health check is always on for every service (5 errors of 500/502/503/504 within a 30s window evicts a backend, with a 30s recovery probe) — it has no ACL keys. See Services.

Sticky sessions

KeyTypeDefaultNotes
cookieStringrequiredCookie name for affinity.

Only the cookie name is configurable. The session TTL (1h) and max-session cap (100,000) are fixed runtime defaults; the cookie is emitted with Path=/; HttpOnly; SameSite=Lax.

Traffic mirroring

Copy a percentage of live traffic to a shadow service (fire-and-forget; the shadow response is discarded and never affects the client).

pub struct MirrorConfig {
    pub service: String, // required, must exist in services
    pub percentage: u8,  // 0-100, default: 100
}
services "api-service" {
  load_balancer {
    strategy = "round-robin"
    servers  = [{ url = "http://127.0.0.1:8001" }]
  }
  mirror {
    service    = "shadow-backend"
    percentage = 10
  }
}

Failover

Route to a fallback service when the primary has zero healthy backends (not per-request retry). The target must exist or the selector is skipped with a warning.

pub struct FailoverConfig {
    pub service: String, // required, must exist in services
}
services "api-service" {
  load_balancer {
    strategy = "round-robin"
    servers  = [{ url = "http://127.0.0.1:8001" }]
  }
  failover {
    service = "backup-pool"
  }
}

Autoscaling (scale-to-zero)

Knative-style serverless serving. The autoscaler loop runs only for services with container_concurrency > 0.

pub struct ScalingConfig {
    pub min_replicas: u32,            // default: 0 (scale-to-zero)
    pub max_replicas: u32,            // default: 10
    pub container_concurrency: u32,   // default: 0 (unlimited)
    pub target_utilization: f64,      // default: 0.7, in (0.0, 1.0]
    pub scale_down_delay_secs: u64,   // default: 300
    pub buffer_timeout_secs: u64,     // default: 30
    pub buffer_size: usize,           // default: 100
    pub buffer_enabled: bool,         // default: false
    pub executor: String,             // "box" (default) | "k8s"
}
KeyTypeDefaultNotes
min_replicasu3200 enables scale-to-zero. Must be <= max_replicas.
max_replicasu3210Upper clamp.
container_concurrencyu320Max concurrent requests per container; 0 = unlimited (also disables the autoscaler + concurrency limiter for the service).
target_utilizationf640.7Must be in (0.0, 1.0].
scale_down_delay_secsu64300Idle seconds before scale-down is allowed.
buffer_timeout_secsu6430How long a buffered request waits during scale-from-zero (then HTTP 504).
buffer_sizeusize100Max requests buffered during scale-from-zero (when full, HTTP 503).
buffer_enabledboolfalseMaster switch for scale-from-zero request buffering.
executorString"box""box" or "k8s". No enum validation; "k8s" and unknown values fall back to the box executor at startup.
services "ai-model" {
  load_balancer {
    servers = [{ url = "http://127.0.0.1:9000" }]
  }
  scaling {
    min_replicas          = 0
    max_replicas          = 20
    container_concurrency = 4
    target_utilization    = 0.7
    scale_down_delay_secs = 120
    buffer_enabled        = true
    buffer_size           = 200
    buffer_timeout_secs   = 30
    executor              = "box"
  }
}

At runtime the autoscaler tick is hardcoded to 2 seconds, in_flight is reported as 0, and the box executor URL is fixed to http://localhost:9090 — so in practice only queued (scale-from-zero) requests drive scaling. executor = "k8s" falls back to the box executor at startup.

Revisions and rollout

Revision-based traffic splitting. The traffic_percent values across all revisions of a service must sum to exactly 100 or config loading fails. Revisions accept an inline revisions = [ { ... } ] list, an object, or repeated revision / revisions child blocks.

pub struct RevisionConfig {
    pub name: String,               // required
    pub traffic_percent: u32,       // 0-100, default: 100
    pub servers: Vec<ServerConfig>, // default: []
    pub strategy: Strategy,         // default: round-robin
}

pub struct RolloutConfig {
    pub from: String,              // required, must name an existing revision
    pub to: String,               // required, must name an existing revision
    pub step_percent: u32,        // default: 10
    pub step_interval_secs: u64,  // default: 60
    pub error_rate_threshold: f64,// default: 0.05
    pub latency_threshold_ms: u64,// default: 5000
}
services "api-service" {
  revisions = [
    {
      name            = "v1"
      traffic_percent = 80
      servers         = [{ url = "http://127.0.0.1:8001" }]
    },
    {
      name            = "v2"
      traffic_percent = 20
      servers         = [{ url = "http://127.0.0.1:8002" }]
    }
  ]

  rollout {
    from                 = "v1"
    to                   = "v2"
    step_percent         = 10
    step_interval_secs   = 60
    error_rate_threshold = 0.05
    latency_threshold_ms = 5000
  }
}

rollout is parsed and validated but inert at runtime in v1.0.11 — no loop drives the rollout controller, so step_interval_secs, step_percent, error_rate_threshold, and latency_threshold_ms have no live effect yet. Revision selection is deterministic weighted round-robin (not random).

See Services for load balancing, health, and scaling behavior in depth.

Middlewares

Middlewares are defined globally and referenced by name in a router's middlewares list. The type key selects the implementation; all HTTP middlewares share one flat config struct, so fields irrelevant to a given type are simply ignored.

There are 14 HTTP-pipeline middleware types selectable via a middlewares block. (A 15th connection-level filter, tcp-filter, is not a middleware type — it is configured on a TCP entrypoint via max_connections / tcp_allowed_ips.)

typeKey fieldsNotes
api-keyheader (default X-API-Key), keys (>= 1 required)Exact key match; missing/invalid → 401.
basic-authusername, password (both required)HTTP Basic; mismatch → 401.
rate-limitrate (required), burst (default = rate)In-process token bucket; over-limit → 429 + Retry-After: 1.
rate-limit-redisredis_url (required), rate (required), burst (default = rate)Distributed bucket; fails open on Redis errors. Needs the redis build feature.
corsallowed_origins, allowed_methods, allowed_headers, max_ageAll have defaults; preflight returns 204, disallowed origin → 403.
headersrequest_headers, response_headersInsert/overwrite headers in/out.
strip-prefixprefixesFirst match wins; /apps/* strips base + one path segment.
ip-allowallowed_ips (>= 1 required)CIDR or single IP; denied → 403.
retrymax_retries (default 3, > 0), retry_interval_ms (default 100)Injects x-gateway-retry-* for the proxy to act on.
jwtvalue (HMAC secret, required), header (default Authorization)HS256 only; exp required; injects x-jwt-subject.
circuit-breakerfailure_threshold (5), cooldown_secs (30), success_threshold (1)Trips after consecutive 5xx; open → 503.
compress(none — ACL config ignored)Hardcoded min_size=1024, level=6; sets x-gateway-compress: eligible.
body-limitmax_body_bytes (required, > 0)Content-Length over limit → 413.
forward-authforward_auth_url (required), forward_auth_response_headersDelegates auth via GET; non-2xx short-circuits.

All key types live on one struct:

pub struct MiddlewareConfig {
    pub middleware_type: String,             // ACL key: "type", required
    pub header: Option<String>,              // api-key / jwt
    pub keys: Vec<String>,                   // api-key
    pub value: Option<String>,               // jwt secret
    pub username: Option<String>,            // basic-auth
    pub password: Option<String>,            // basic-auth
    pub rate: Option<u64>,                   // rate-limit / rate-limit-redis
    pub burst: Option<u64>,                  // rate-limit / rate-limit-redis
    pub allowed_origins: Vec<String>,        // cors
    pub allowed_methods: Vec<String>,        // cors
    pub allowed_headers: Vec<String>,        // cors
    pub max_age: Option<u64>,                // cors
    pub request_headers: HashMap<String, String>,  // headers
    pub response_headers: HashMap<String, String>, // headers
    pub prefixes: Vec<String>,               // strip-prefix
    pub max_retries: Option<u32>,            // retry
    pub retry_interval_ms: Option<u64>,      // retry
    pub allowed_ips: Vec<String>,            // ip-allow
    pub forward_auth_url: Option<String>,    // forward-auth
    pub forward_auth_response_headers: Vec<String>, // forward-auth
    pub redis_url: Option<String>,           // rate-limit-redis
    pub max_body_bytes: Option<u64>,         // body-limit
    pub failure_threshold: Option<u32>,      // circuit-breaker
    pub cooldown_secs: Option<u64>,          // circuit-breaker
    pub success_threshold: Option<u32>,      // circuit-breaker
}
middlewares "auth-jwt" {
  type  = "jwt"
  value = env("JWT_SECRET")
}

middlewares "api-key" {
  type   = "api-key"
  header = "X-API-Key"
  keys   = ["key-abc", "key-def"]
}

middlewares "rate-limit" {
  type  = "rate-limit"
  rate  = 100
  burst = 50
}

middlewares "cors" {
  type            = "cors"
  allowed_origins = ["https://example.com"]
  allowed_methods = ["GET", "POST", "PUT", "DELETE"]
  allowed_headers = ["Content-Type", "Authorization"]
  max_age         = 3600
}

middlewares "strip-apps" {
  type     = "strip-prefix"
  prefixes = ["/apps/*"]
}

middlewares "circuit-breaker" {
  type              = "circuit-breaker"
  failure_threshold = 3
  cooldown_secs     = 60
  success_threshold = 2
}

middlewares "edge-auth" {
  type                          = "forward-auth"
  forward_auth_url              = "http://auth-svc:8080/verify"
  forward_auth_response_headers = ["X-User-Id", "X-User-Roles"]
}

request_headers / response_headers are written in ACL as a list of { name = ..., value = ... } objects (the parser also accepts key/header for the name), not as a native map literal.

See Middleware for full behavior of every type.

Observability

A single top-level block with three boolean flags, all defaulting to true. These are checked per request on the hot path.

pub struct ObservabilityConfig {
    pub metrics_enabled: bool,    // default: true
    pub access_log_enabled: bool, // default: true
    pub tracing_enabled: bool,    // default: true
}
KeyTypeDefaultNotes
metrics_enabledbooltruePrometheus per-router / -service / -backend counters.
access_log_enabledbooltruePer-request access logging.
tracing_enabledbooltrueW3C Trace Context extraction + outbound traceparent injection.
observability {
  metrics_enabled    = true
  access_log_enabled = true
  tracing_enabled    = true
}

There are no otlp / sample_rate / propagation / service_name keys in this block. See Observability for which metrics and trace formats are actually emitted in v1.0.11.

Management

The Dashboard / management API is an optional, dedicated listener (disabled by default, bound to 127.0.0.1:9090) that never intercepts traffic entrypoints. There is no separate dashboard block — this is it. Requests are guarded in order: path-prefix match → IP allowlist → bearer token, with optional TLS/mTLS. Rejected requests and TLS handshake failures are retained in an in-memory audit ring buffer exposed at {path_prefix}/events.

pub struct ManagementConfig {
    pub enabled: bool,                  // default: false
    pub address: String,                // default: "127.0.0.1:9090"
    pub path_prefix: String,            // default: "/api/gateway"
    pub auth_token_env: Option<String>, // default: "A3S_GATEWAY_ADMIN_TOKEN"
    pub allowed_ips: Vec<String>,       // default: ["127.0.0.1", "::1"]
    pub tls: Option<ManagementTlsConfig>,
}

pub struct ManagementTlsConfig {
    pub cert_file: String,              // required
    pub key_file: String,              // required
    pub client_ca_file: Option<String>, // default: none
    pub require_client_cert: bool,      // default: false
    pub min_version: String,            // "1.2" (default) or "1.3"
}
KeyTypeDefaultNotes
enabledboolfalseMaster switch for the listener.
addressString127.0.0.1:9090Validated as a socket address when enabled.
path_prefixString/api/gatewayMust start with / when enabled.
auth_token_envStringA3S_GATEWAY_ADMIN_TOKENName of the env var holding the bearer token. Setting it to "" disables token auth.
allowed_ips[String]["127.0.0.1", "::1"]CIDR or single IP. Omitting keeps loopback default; an explicit [] disables IP filtering.
tlsblocknoneOptional TLS / mTLS.

Management TLS keys:

KeyTypeDefaultNotes
cert_fileStringrequiredServer cert PEM; rejected if empty.
key_fileStringrequiredServer key PEM; rejected if empty.
client_ca_fileStringnoneCA to verify client certs; required if require_client_cert = true.
require_client_certboolfalseEnforce mTLS.
min_versionString"1.2"Validated to be exactly "1.2" or "1.3".
management {
  enabled        = true
  address        = "127.0.0.1:9090"
  path_prefix    = "/api/gateway"
  auth_token_env = "A3S_GATEWAY_ADMIN_TOKEN"
  allowed_ips    = ["127.0.0.1", "::1"]

  tls {
    cert_file           = "/etc/a3s/admin/server.crt"
    key_file            = "/etc/a3s/admin/server.key"
    client_ca_file      = "/etc/a3s/admin/client-ca.crt"
    require_client_cert = true
    min_version         = "1.3"
  }
}

If auth_token_env names a variable that is unset at startup, the management listener fails to start. Set auth_token_env = "" to disable bearer auth, or allowed_ips = [] to disable IP filtering — these are distinct from omitting the keys.

Providers

Dynamic configuration sources. Only four blocks are recognized under providers (file, discovery, kubernetes, docker); any other is rejected (Unknown providers ACL block). Discovered entries are added to static config, and static config wins on name collisions (except the Docker provider, which overwrites). There is no dns provider in v1.0.11.

File provider

KeyTypeDefaultNotes
watchbooltrueHot-reload on change (only .acl fragments are recognized).
directoryStringnoneExtra directory whose .acl fragments are merged.
providers {
  file {
    watch     = true
    directory = "/etc/gateway/conf.d/"
  }
}

When watch = true, the gateway watches the config file's parent directory (and the optional directory recursively) via notify (inotify / kqueue / ReadDirectoryChanges), debounces changes (500ms), re-parses, validates, and hot-reloads without downtime.

Discovery provider

Health-based service discovery that polls backend seed URLs for /.well-known/a3s-service.json metadata, then probes each seed's health path.

KeyTypeDefaultNotes
seedslist of { url }requiredBackend seed URLs.
poll_interval_secsu6430Poll cadence.
timeout_secsu645Per-probe HTTP timeout.
providers {
  discovery {
    poll_interval_secs = 30
    timeout_secs       = 5
    seeds = [
      { url = "http://10.0.0.5:8080" },
      { url = "http://10.0.0.6:8080" }
    ]
  }
}

Docker provider

Auto-discover services from container labels (under label_prefix, e.g. a3s.enable, a3s.router.rule, a3s.service.port).

KeyTypeDefaultNotes
hostString/var/run/docker.sockUnix socket path or tcp:// URL.
label_prefixStringa3sPrefix for routing labels.
poll_interval_secsu6410Poll cadence (clamped to >= 1).
providers {
  docker {
    host               = "/var/run/docker.sock"
    label_prefix       = "a3s"
    poll_interval_secs = 10
  }
}

A container needs <prefix>.enable = "true" (exact string) and <prefix>.service.port to be picked up. See Routing for the full Docker label and Kubernetes annotation reference.

Kubernetes provider

Watch Ingress resources (and, optionally, ConfigMap-backed IngressRoute specs). Requires the gateway to be built with the kube feature (published Linux images enable it); without it, the block only logs a warning.

KeyTypeDefaultNotes
namespaceString""Empty = all namespaces.
label_selectorString""Applied to Ingress listing only when non-empty.
watch_interval_secsu6430Poll cadence for both watchers.
ingress_route_crdboolfalseAlso watch IngressRoute specs (stored in ConfigMaps labeled a3s-gateway.io/type=ingressroute).
providers {
  kubernetes {
    namespace           = "default"
    label_selector      = "app=my-service"
    watch_interval_secs = 30
    ingress_route_crd   = true
  }
}

Hot reload

let new_config = GatewayConfig::from_file("gateway.acl").await?;
gateway.reload(new_config).await?;

During reload:

  1. The new configuration is validated; on failure the previous config is kept.
  2. Routers, services, and middlewares are rebuilt.
  3. If old.entrypoints == new.entrypoints and neither config has a UDP entrypoint, the runtime snapshot is hot-swapped without rebinding traffic ports.
  4. If HTTP/TCP entrypoints were added or changed (and no UDP), only the changed listeners are restarted; unchanged listeners stay bound.
  5. If any UDP entrypoint is involved, a full abort-and-restart is performed.

File-watch hot reload only runs when a providers.file block is present and watch = true.

Full example

shutdown_timeout_secs {
  shutdown_timeout_secs = 30
}

# Observability
observability {
  metrics_enabled    = true
  access_log_enabled = true
  tracing_enabled    = true
}

# Entrypoints
entrypoints "web" {
  address = "0.0.0.0:80"
}

entrypoints "websecure" {
  address = "0.0.0.0:443"
  tls {
    cert_file = "/etc/certs/cert.pem"
    key_file  = "/etc/certs/key.pem"
  }
}

# Routers
routers "api" {
  rule        = "Host(`api.example.com`) && PathPrefix(`/v1`)"
  service     = "api-service"
  entrypoints = ["websecure"]
  middlewares = ["auth-jwt", "rate-limit", "cors"]
  priority    = 100
}

routers "web" {
  rule        = "Host(`www.example.com`)"
  service     = "web-service"
  entrypoints = ["web", "websecure"]
}

# Services
services "api-service" {
  load_balancer {
    strategy        = "least-connections"
    request_timeout = "30s"
    servers = [
      { url = "http://127.0.0.1:8001" },
      { url = "http://127.0.0.1:8002" }
    ]
    health_check {
      path     = "/health"
      interval = "10s"
    }
  }
}

services "web-service" {
  load_balancer {
    strategy = "round-robin"
    servers  = [{ url = "http://127.0.0.1:3000" }]
  }
}

# Middlewares
middlewares "auth-jwt" {
  type  = "jwt"
  value = env("JWT_SECRET")
}

middlewares "rate-limit" {
  type  = "rate-limit"
  rate  = 100
  burst = 50
}

middlewares "cors" {
  type            = "cors"
  allowed_origins = ["https://example.com"]
  allowed_methods = ["GET", "POST"]
}

# Management API
management {
  enabled        = true
  auth_token_env = "A3S_GATEWAY_ADMIN_TOKEN"
}

# Providers
providers {
  file {
    watch = true
  }
}

On this page