Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
69 changes: 69 additions & 0 deletions .examples/docker-compose-oidc/compose.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
# Note that this example is using network_mode: host, which may have security implications.
# It is done this way for simplicity, to avoid complications that come from the fact that
# Gatus NEEDS to see authentication provider under same address as the one used by the end-user.
# This way both Gatus and the end-user see the authentication provider under "localhost", what
# is convenient for local testing.
services:
dex:
network_mode: host
image: ghcr.io/dexidp/dex:latest
command: ["dex", "serve", "/etc/dex/config.yaml"]
configs:
- source: dex_config_yaml
target: /etc/dex/config.yaml
healthcheck:
test: ["CMD", "wget", "-qO", "/dev/null", "http://localhost:5556/healthz"]
interval: 1s
timeout: 1s
retries: 5
start_period: 1s

gatus:
network_mode: host
image: twinproduction/gatus:latest
volumes:
- ../../config.yml:/config/02_config.yaml
environment:
- GATUS_CONFIG_PATH=/config
configs:
- source: gatus_config_ext
target: /config/01_config.yaml
depends_on:
dex:
condition: service_healthy

configs:
dex_config_yaml:
content: |
issuer: http://localhost:5556/
storage:
type: sqlite3
config:
file: /var/dex/dex.db
web:
http: 0.0.0.0:5556
enablePasswordDB: true
staticClients:
- id: gatus
name: 'Gatus'
secret: gatus-client-secret
redirectURIs:
- 'http://localhost:8080/authorization-code/callback'
staticPasswords:
- email: "[email protected]"
# bcrypt hash of the string "password": $(echo password | htpasswd -BinC 10 admin | cut -d: -f2)
hash: "$2a$10$2b2cU8CPhOTaGrs1HRQuAueS7JTT5ZHsHSzYiFPm1leZck7Mc8T4W"
username: "admin"
userID: "08a8684b-db88-4b73-90a9-3cd1661f5466"

gatus_config_ext:
content: |
security:
oidc:
issuer-url: "http://localhost:5556/"
redirect-url: "http://localhost:8080/authorization-code/callback"
client-id: "gatus"
client-secret: "gatus-client-secret"
scopes:
- openid

93 changes: 93 additions & 0 deletions .examples/docker-compose-reverse-proxy-oidc/compose.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
# Note that this example is using network_mode: host, which may have security implications.
# It is done this way for simplicity, to avoid complications that come from the fact that
# Gatus NEEDS to see authentication provider under same address as the one used by the end-user.
# This way both Gatus and the end-user see the authentication provider under "localhost", what
# is convenient for local testing.
services:
dex:
network_mode: host
image: ghcr.io/dexidp/dex:latest
command: ["dex", "serve", "/etc/dex/config.yaml"]
configs:
- source: dex_config_yaml
target: /etc/dex/config.yaml
healthcheck:
test: ["CMD", "wget", "-qO", "/dev/null", "http://localhost:5556/healthz"]
interval: 10s
timeout: 5s
retries: 5
start_period: 1s

caddy:
network_mode: host
image: caddy:latest
container_name: gatus_proxy
configs:
- source: caddyfile
target: /etc/caddy/Caddyfile

gatus:
network_mode: host
image: twinproduction/gatus:latest
container_name: gatus_behind_proxy
volumes:
- ../../config.yml:/config/02_config.yaml
environment:
- GATUS_CONFIG_PATH=/config
configs:
- source: gatus_config_ext
target: /config/01_config.yaml
depends_on:
dex:
condition: service_healthy


configs:
dex_config_yaml:
content: |
issuer: http://localhost:5556/
storage:
type: sqlite3
config:
file: /var/dex/dex.db
web:
http: 0.0.0.0:5556
enablePasswordDB: true
staticClients:
- id: gatus
name: 'Gatus'
secret: gatus-client-secret
redirectURIs:
- 'http://localhost:8080/gatus/authorization-code/callback'
staticPasswords:
- email: "[email protected]"
# bcrypt hash of the string "password": $(echo password | htpasswd -BinC 10 admin | cut -d: -f2)
hash: "$2a$10$2b2cU8CPhOTaGrs1HRQuAueS7JTT5ZHsHSzYiFPm1leZck7Mc8T4W"
username: "admin"
userID: "08a8684b-db88-4b73-90a9-3cd1661f5466"

gatus_config_ext:
content: |
web:
base-path: /gatus/
port: 8081
security:
oidc:
issuer-url: "http://localhost:5556/"
redirect-url: "http://localhost:8080/gatus/authorization-code/callback"
client-id: "gatus"
client-secret: "gatus-client-secret"
scopes:
- openid
caddyfile:
content: |
{
auto_https off
http_port 8080
}

http://localhost:8080 {
handle_path /gatus/* {
reverse_proxy http://localhost:8081
}
}
46 changes: 46 additions & 0 deletions .examples/docker-compose-reverse-proxy/compose.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
networks:
caddy_network:
name: gatus_proxy_network

services:
caddy:
networks:
- caddy_network
image: caddy:latest
container_name: gatus_proxy
ports:
- 8080:8080
configs:
- source: caddyfile
target: /etc/caddy/Caddyfile
gatus:
networks:
- caddy_network
image: twinproduction/gatus:latest
container_name: gatus_behind_proxy

volumes:
- ../../config.yml:/config/02_config.yaml
environment:
- GATUS_CONFIG_PATH=/config
configs:
- source: gatus_config_ext
target: /config/01_config.yaml

configs:
gatus_config_ext:
content: |
web:
base-path: /gatus/
caddyfile:
content: |
{
auto_https off
http_port 8080
}

http://localhost:8080 {
handle_path /gatus/* {
reverse_proxy http://gatus_behind_proxy:8080
}
}
14 changes: 12 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -257,6 +257,7 @@ If you want to test it locally, see [Docker](#docker).
| `web` | Web configuration. | `{}` |
| `web.address` | Address to listen on. | `0.0.0.0` |
| `web.port` | Port to listen on. | `8080` |
| `web.base-path` | `href` attribute of the HTML `<base>` tag. Use this if you want to host Gatus on a subpath (e.g. `/status/`). Has to end with '/'. | `/` |
| `web.read-buffer-size` | Buffer size for reading requests from a connection. Also limit for the maximum header size. | `8192` |
| `web.tls.certificate-file` | Optional public certificate file for TLS in PEM format. | `""` |
| `web.tls.private-key-file` | Optional private key file for TLS in PEM format. | `""` |
Expand Down Expand Up @@ -2665,6 +2666,8 @@ security:

Confused? Read [Securing Gatus with OIDC using Auth0](https://twin.sh/articles/56/securing-gatus-with-oidc-using-auth0).

The example configuration using [Dex](https://dexidp.io/) as the OIDC provider,
can be found in [.examples/docker-compose-oidc](.examples/docker-compose-oidc).

### TLS Encryption
Gatus supports basic encryption with TLS. To enable this, certificate files in PEM format have to be provided.
Expand Down Expand Up @@ -3271,10 +3274,17 @@ clears their browser's localstorage.


### Exposing Gatus on a custom path
Currently, you can expose the Gatus UI using a fully qualified domain name (FQDN) such as `status.example.org`. However, it does not support path-based routing, which means you cannot expose it through a URL like `example.org/status/`.
Gatus always exposes its endpoints and UI under the root path ('/'), and it cannot be changed.
However, if you find yourself in a situation where you need to expose Gatus on a custom path (e.g. `/gatus/`),
you can achieve this by using a reverse proxy such as Nginx or Traefik or Caddy to handle the path rewriting for you.
In order for it to work properly, you must tell Gatus to use the custom path as its `web.base-path` so that all links
are generated correctly and redirections work as expected.

For more information, see https://github.com/TwiN/gatus/issues/88.
See [examples/docker-compose-reverse-proxy](.examples/docker-compose-reverse-proxy) for example using Caddy.

Note that if you are using OIDC for authentication, your `security.oidc.redirect-url` must include the custom path as well.

See [examples/docker-compose-reverse-proxy-oidc](.examples/docker-compose-reverse-proxy-oidc) for an example using Caddy and OIDC.

### Exposing Gatus on a custom port
By default, Gatus is exposed on port `8080`, but you may specify a different port by setting the `web.port` parameter:
Expand Down
10 changes: 7 additions & 3 deletions config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -301,13 +301,13 @@ func parseAndValidateConfigBytes(yamlBytes []byte) (config *Config, err error) {
}
// XXX: End of v6.0.0 removals
ValidateAlertingConfig(config.Alerting, config.Endpoints, config.ExternalEndpoints)
if err := ValidateSecurityConfig(config); err != nil {
if err := ValidateWebConfig(config); err != nil {
return nil, err
}
if err := ValidateEndpointsConfig(config); err != nil {
if err := ValidateSecurityConfig(config); err != nil {
return nil, err
}
if err := ValidateWebConfig(config); err != nil {
if err := ValidateEndpointsConfig(config); err != nil {
return nil, err
}
if err := ValidateUIConfig(config); err != nil {
Expand Down Expand Up @@ -340,6 +340,10 @@ func parseAndValidateConfigBytes(yamlBytes []byte) (config *Config, err error) {
ValidateAndSetConcurrencyDefaults(config)
// Cross-config changes
config.UI.MaximumNumberOfResults = config.Storage.MaximumNumberOfResults
config.UI.BasePath = config.Web.BasePath
if config.Security != nil && config.Security.OIDC != nil {
config.Security.OIDC.BasePath = config.Web.BasePath
}
}
return
}
Expand Down
10 changes: 9 additions & 1 deletion config/ui/ui.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ const (
defaultCustomCSS = ""
defaultSortBy = "name"
defaultFilterBy = "none"
defaultBasePath = "/"
)

var (
Expand All @@ -44,10 +45,13 @@ type Config struct {
DarkMode *bool `yaml:"dark-mode,omitempty"` // DarkMode is a flag to enable dark mode by default
DefaultSortBy string `yaml:"default-sort-by,omitempty"` // DefaultSortBy is the default sort option ('name', 'group', 'health')
DefaultFilterBy string `yaml:"default-filter-by,omitempty"` // DefaultFilterBy is the default filter option ('none', 'failing', 'unstable')

//////////////////////////////////////////////
// Non-configurable - used for UI rendering //
//////////////////////////////////////////////
MaximumNumberOfResults int `yaml:"-"` // MaximumNumberOfResults to display on the page, it's not configurable because we're passing it from the storage config

MaximumNumberOfResults int `yaml:"-"` // MaximumNumberOfResults to display on the page, it's not configurable because we're passing it from the storage config
BasePath string `yaml:"-"` // basePath is from Web.BasePath
}

func (cfg *Config) IsDarkMode() bool {
Expand Down Expand Up @@ -86,6 +90,7 @@ func GetDefaultConfig() *Config {
DefaultSortBy: defaultSortBy,
DefaultFilterBy: defaultFilterBy,
MaximumNumberOfResults: storage.DefaultMaximumNumberOfResults,
BasePath: defaultBasePath,
}
}

Expand Down Expand Up @@ -133,6 +138,9 @@ func (cfg *Config) ValidateAndSetDefaults() error {
return err
}
}
if len(cfg.BasePath) == 0 {
cfg.BasePath = defaultBasePath
}
// Validate that the template works
t, err := template.ParseFS(static.FileSystem, static.IndexPath)
if err != nil {
Expand Down
13 changes: 13 additions & 0 deletions config/ui/ui_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,9 @@ func TestConfig_ValidateAndSetDefaults(t *testing.T) {
if cfg.DefaultFilterBy != defaultFilterBy {
t.Errorf("expected defaultFilterBy to be %s, got %s", defaultFilterBy, cfg.DefaultFilterBy)
}
if cfg.BasePath != defaultBasePath {
t.Errorf("expected BasePath to be %s, got %s", defaultBasePath, cfg.BasePath)
}
})
t.Run("custom-values", func(t *testing.T) {
cfg := &Config{
Expand All @@ -53,6 +56,7 @@ func TestConfig_ValidateAndSetDefaults(t *testing.T) {
Link: "https://example.com",
DefaultSortBy: "health",
DefaultFilterBy: "failing",
BasePath: "/custom-base-path/",
}
if err := cfg.ValidateAndSetDefaults(); err != nil {
t.Error("expected no error, got", err.Error())
Expand Down Expand Up @@ -84,6 +88,9 @@ func TestConfig_ValidateAndSetDefaults(t *testing.T) {
if cfg.DefaultFilterBy != "failing" {
t.Errorf("expected defaultFilterBy to be preserved, got %s", cfg.DefaultFilterBy)
}
if cfg.BasePath != "/custom-base-path/" {
t.Errorf("expected BasePath to be /custom-base-path/, got %s", cfg.BasePath)
}
})
t.Run("partial-custom-values", func(t *testing.T) {
cfg := &Config{
Expand All @@ -110,6 +117,9 @@ func TestConfig_ValidateAndSetDefaults(t *testing.T) {
if cfg.Description != defaultDescription {
t.Errorf("expected description to use default, got %s", cfg.Description)
}
if cfg.BasePath != defaultBasePath {
t.Errorf("expected BasePath to be %s, got %s", defaultBasePath, cfg.BasePath)
}
})
}

Expand Down Expand Up @@ -172,6 +182,9 @@ func TestGetDefaultConfig(t *testing.T) {
if defaultConfig.DefaultFilterBy != defaultFilterBy {
t.Error("expected GetDefaultConfig() to return defaultFilterBy, got", defaultConfig.DefaultFilterBy)
}
if defaultConfig.BasePath != defaultBasePath {
t.Error("expected GetDefaultConfig() to return defaultBasePath, got", defaultConfig.BasePath)
}
}

func TestConfig_ValidateAndSetDefaults_DefaultSortBy(t *testing.T) {
Expand Down
Loading