first commit
This commit is contained in:
223
volumes/api/envoy/cds.yaml
Normal file
223
volumes/api/envoy/cds.yaml
Normal file
@@ -0,0 +1,223 @@
|
||||
resources:
|
||||
- '@type': type.googleapis.com/envoy.config.cluster.v3.Cluster
|
||||
name: auth
|
||||
connect_timeout: 5s
|
||||
type: STRICT_DNS
|
||||
dns_refresh_rate: 5s
|
||||
dns_failure_refresh_rate:
|
||||
base_interval: 1s
|
||||
max_interval: 1s
|
||||
lb_policy: ROUND_ROBIN
|
||||
load_assignment:
|
||||
cluster_name: auth
|
||||
endpoints:
|
||||
- lb_endpoints:
|
||||
- endpoint:
|
||||
address:
|
||||
socket_address:
|
||||
address: auth
|
||||
port_value: 9999
|
||||
health_checks:
|
||||
- timeout: 2s
|
||||
interval: 5s
|
||||
unhealthy_threshold: 3
|
||||
healthy_threshold: 2
|
||||
http_health_check:
|
||||
path: /health
|
||||
circuit_breakers:
|
||||
thresholds:
|
||||
- priority: DEFAULT
|
||||
max_connections: 10000
|
||||
max_pending_requests: 10000
|
||||
max_requests: 10000
|
||||
|
||||
- '@type': type.googleapis.com/envoy.config.cluster.v3.Cluster
|
||||
name: rest
|
||||
connect_timeout: 5s
|
||||
type: STRICT_DNS
|
||||
dns_refresh_rate: 5s
|
||||
dns_failure_refresh_rate:
|
||||
base_interval: 1s
|
||||
max_interval: 1s
|
||||
lb_policy: ROUND_ROBIN
|
||||
load_assignment:
|
||||
cluster_name: rest
|
||||
endpoints:
|
||||
- lb_endpoints:
|
||||
- endpoint:
|
||||
address:
|
||||
socket_address:
|
||||
address: rest
|
||||
port_value: 3000
|
||||
health_checks:
|
||||
- timeout: 2s
|
||||
interval: 5s
|
||||
unhealthy_threshold: 3
|
||||
healthy_threshold: 2
|
||||
http_health_check:
|
||||
path: /
|
||||
circuit_breakers:
|
||||
thresholds:
|
||||
- priority: DEFAULT
|
||||
max_connections: 10000
|
||||
max_pending_requests: 10000
|
||||
max_requests: 10000
|
||||
|
||||
- '@type': type.googleapis.com/envoy.config.cluster.v3.Cluster
|
||||
name: realtime
|
||||
connect_timeout: 5s
|
||||
type: STRICT_DNS
|
||||
dns_refresh_rate: 5s
|
||||
dns_failure_refresh_rate:
|
||||
base_interval: 1s
|
||||
max_interval: 1s
|
||||
lb_policy: ROUND_ROBIN
|
||||
load_assignment:
|
||||
cluster_name: realtime
|
||||
endpoints:
|
||||
- lb_endpoints:
|
||||
- endpoint:
|
||||
address:
|
||||
socket_address:
|
||||
address: realtime-dev.supabase-realtime
|
||||
port_value: 4000
|
||||
health_checks:
|
||||
- timeout: 2s
|
||||
interval: 5s
|
||||
unhealthy_threshold: 3
|
||||
healthy_threshold: 2
|
||||
http_health_check:
|
||||
path: /
|
||||
circuit_breakers:
|
||||
thresholds:
|
||||
- priority: DEFAULT
|
||||
max_connections: 10000
|
||||
max_pending_requests: 10000
|
||||
max_requests: 10000
|
||||
|
||||
- '@type': type.googleapis.com/envoy.config.cluster.v3.Cluster
|
||||
name: storage
|
||||
connect_timeout: 5s
|
||||
type: STRICT_DNS
|
||||
dns_refresh_rate: 5s
|
||||
dns_failure_refresh_rate:
|
||||
base_interval: 1s
|
||||
max_interval: 1s
|
||||
lb_policy: ROUND_ROBIN
|
||||
load_assignment:
|
||||
cluster_name: storage
|
||||
endpoints:
|
||||
- lb_endpoints:
|
||||
- endpoint:
|
||||
address:
|
||||
socket_address:
|
||||
address: storage
|
||||
port_value: 5000
|
||||
health_checks:
|
||||
- timeout: 2s
|
||||
interval: 5s
|
||||
unhealthy_threshold: 3
|
||||
healthy_threshold: 2
|
||||
http_health_check:
|
||||
path: /status
|
||||
circuit_breakers:
|
||||
thresholds:
|
||||
- priority: DEFAULT
|
||||
max_connections: 10000
|
||||
max_pending_requests: 10000
|
||||
max_requests: 10000
|
||||
|
||||
- '@type': type.googleapis.com/envoy.config.cluster.v3.Cluster
|
||||
name: functions
|
||||
connect_timeout: 5s
|
||||
type: STRICT_DNS
|
||||
dns_refresh_rate: 5s
|
||||
dns_failure_refresh_rate:
|
||||
base_interval: 1s
|
||||
max_interval: 1s
|
||||
lb_policy: ROUND_ROBIN
|
||||
load_assignment:
|
||||
cluster_name: functions
|
||||
endpoints:
|
||||
- lb_endpoints:
|
||||
- endpoint:
|
||||
address:
|
||||
socket_address:
|
||||
address: functions
|
||||
port_value: 9000
|
||||
health_checks:
|
||||
- timeout: 2s
|
||||
interval: 5s
|
||||
unhealthy_threshold: 3
|
||||
healthy_threshold: 2
|
||||
tcp_health_check: {}
|
||||
circuit_breakers:
|
||||
thresholds:
|
||||
- priority: DEFAULT
|
||||
max_connections: 10000
|
||||
max_pending_requests: 10000
|
||||
max_requests: 10000
|
||||
|
||||
- '@type': type.googleapis.com/envoy.config.cluster.v3.Cluster
|
||||
name: meta
|
||||
connect_timeout: 5s
|
||||
type: STRICT_DNS
|
||||
dns_refresh_rate: 5s
|
||||
dns_failure_refresh_rate:
|
||||
base_interval: 1s
|
||||
max_interval: 1s
|
||||
lb_policy: ROUND_ROBIN
|
||||
load_assignment:
|
||||
cluster_name: meta
|
||||
endpoints:
|
||||
- lb_endpoints:
|
||||
- endpoint:
|
||||
address:
|
||||
socket_address:
|
||||
address: meta
|
||||
port_value: 8080
|
||||
health_checks:
|
||||
- timeout: 2s
|
||||
interval: 5s
|
||||
unhealthy_threshold: 3
|
||||
healthy_threshold: 2
|
||||
http_health_check:
|
||||
path: /health
|
||||
circuit_breakers:
|
||||
thresholds:
|
||||
- priority: DEFAULT
|
||||
max_connections: 10000
|
||||
max_pending_requests: 10000
|
||||
max_requests: 10000
|
||||
|
||||
- '@type': type.googleapis.com/envoy.config.cluster.v3.Cluster
|
||||
name: studio
|
||||
connect_timeout: 5s
|
||||
type: STRICT_DNS
|
||||
dns_refresh_rate: 5s
|
||||
dns_failure_refresh_rate:
|
||||
base_interval: 1s
|
||||
max_interval: 1s
|
||||
lb_policy: ROUND_ROBIN
|
||||
load_assignment:
|
||||
cluster_name: studio
|
||||
endpoints:
|
||||
- lb_endpoints:
|
||||
- endpoint:
|
||||
address:
|
||||
socket_address:
|
||||
address: studio
|
||||
port_value: 3000
|
||||
health_checks:
|
||||
- timeout: 2s
|
||||
interval: 5s
|
||||
unhealthy_threshold: 3
|
||||
healthy_threshold: 2
|
||||
http_health_check:
|
||||
path: /project/default
|
||||
circuit_breakers:
|
||||
thresholds:
|
||||
- priority: DEFAULT
|
||||
max_connections: 10000
|
||||
max_pending_requests: 10000
|
||||
max_requests: 10000
|
||||
34
volumes/api/envoy/docker-entrypoint.sh
Executable file
34
volumes/api/envoy/docker-entrypoint.sh
Executable file
@@ -0,0 +1,34 @@
|
||||
#!/bin/sh
|
||||
set -e
|
||||
|
||||
# Generate SHA1 base64 hash for Envoy basic auth user list
|
||||
PASSWORD_HASH=$(printf '%s' "${DASHBOARD_PASSWORD}" | openssl sha1 -binary | openssl base64)
|
||||
DASHBOARD_BASIC_AUTH="${DASHBOARD_USERNAME}:{SHA}${PASSWORD_HASH}"
|
||||
|
||||
echo "Generating Envoy configuration..."
|
||||
|
||||
# Process the lds.yaml template with environment variables using sed
|
||||
# Using | as delimiter since JWT tokens contain /
|
||||
sed -e "s|\${ANON_KEY}|${ANON_KEY}|g" \
|
||||
-e "s|\${ANON_KEY_ASYMMETRIC}|${ANON_KEY_ASYMMETRIC}|g" \
|
||||
-e "s|\${SERVICE_ROLE_KEY}|${SERVICE_ROLE_KEY}|g" \
|
||||
-e "s|\${SERVICE_ROLE_KEY_ASYMMETRIC}|${SERVICE_ROLE_KEY_ASYMMETRIC}|g" \
|
||||
-e "s|\${SUPABASE_PUBLISHABLE_KEY}|${SUPABASE_PUBLISHABLE_KEY}|g" \
|
||||
-e "s|\${SUPABASE_SECRET_KEY}|${SUPABASE_SECRET_KEY}|g" \
|
||||
-e "s|\${DASHBOARD_BASIC_AUTH}|${DASHBOARD_BASIC_AUTH}|g" \
|
||||
/etc/envoy/lds.template.yaml > /etc/envoy/lds.yaml
|
||||
|
||||
if [ -n "$SUPABASE_SECRET_KEY" ] && \
|
||||
[ -n "$SUPABASE_PUBLISHABLE_KEY" ] && \
|
||||
[ -n "$SERVICE_ROLE_KEY_ASYMMETRIC" ] && \
|
||||
[ -n "$ANON_KEY_ASYMMETRIC" ]; then
|
||||
echo "Envoy sb_ key translation enabled"
|
||||
else
|
||||
echo "Envoy running in legacy API key mode (sb_ keys disabled)"
|
||||
fi
|
||||
|
||||
echo "Envoy configuration generated successfully"
|
||||
echo "Starting Envoy..."
|
||||
|
||||
# Start Envoy
|
||||
exec envoy -c /etc/envoy/envoy.yaml "$@"
|
||||
27
volumes/api/envoy/envoy.yaml
Normal file
27
volumes/api/envoy/envoy.yaml
Normal file
@@ -0,0 +1,27 @@
|
||||
dynamic_resources:
|
||||
cds_config:
|
||||
path_config_source:
|
||||
path: /etc/envoy/cds.yaml
|
||||
resource_api_version: V3
|
||||
lds_config:
|
||||
path_config_source:
|
||||
path: /etc/envoy/lds.yaml
|
||||
resource_api_version: V3
|
||||
|
||||
node:
|
||||
cluster: supabase_cluster
|
||||
id: supabase_node
|
||||
|
||||
overload_manager:
|
||||
resource_monitors:
|
||||
- name: envoy.resource_monitors.global_downstream_max_connections
|
||||
typed_config:
|
||||
'@type': >-
|
||||
type.googleapis.com/envoy.extensions.resource_monitors.downstream_connections.v3.DownstreamConnectionsConfig
|
||||
max_active_downstream_connections: 30000
|
||||
|
||||
admin:
|
||||
address:
|
||||
socket_address:
|
||||
address: 127.0.0.1
|
||||
port_value: 9901
|
||||
994
volumes/api/envoy/lds.template.yaml
Normal file
994
volumes/api/envoy/lds.template.yaml
Normal file
@@ -0,0 +1,994 @@
|
||||
resources:
|
||||
- '@type': type.googleapis.com/envoy.config.listener.v3.Listener
|
||||
name: supabase
|
||||
per_connection_buffer_limit_bytes: 32768 # 32 KiB
|
||||
|
||||
address:
|
||||
socket_address:
|
||||
address: 0.0.0.0
|
||||
port_value: 8000
|
||||
|
||||
filter_chains:
|
||||
- filters:
|
||||
- name: envoy.filters.network.http_connection_manager
|
||||
typed_config:
|
||||
'@type': >-
|
||||
type.googleapis.com/envoy.extensions.filters.network.http_connection_manager.v3.HttpConnectionManager
|
||||
stat_prefix: ingress_http
|
||||
normalize_path: true
|
||||
merge_slashes: true
|
||||
path_with_escaped_slashes_action: REJECT_REQUEST
|
||||
use_remote_address: true
|
||||
common_http_protocol_options:
|
||||
headers_with_underscores_action: REJECT_REQUEST
|
||||
upgrade_configs:
|
||||
- upgrade_type: websocket
|
||||
access_log:
|
||||
- name: envoy.access_loggers.stdout
|
||||
typed_config:
|
||||
'@type': >-
|
||||
type.googleapis.com/envoy.extensions.access_loggers.stream.v3.StdoutAccessLog
|
||||
log_format:
|
||||
text_format_source:
|
||||
inline_string: "%DOWNSTREAM_REMOTE_ADDRESS_WITHOUT_PORT% - - [%START_TIME(%d/%b/%Y:%H:%M:%S %z)%] \"%REQ(:METHOD)% %REQ(X-ENVOY-ORIGINAL-PATH?:PATH)% %PROTOCOL%\" %RESPONSE_CODE% %BYTES_SENT% \"%REQ(REFERER)%\" \"%REQ(USER-AGENT)%\"\n"
|
||||
|
||||
route_config:
|
||||
name: supabase_route
|
||||
virtual_hosts:
|
||||
- name: supabase_host
|
||||
domains:
|
||||
- '*'
|
||||
cors:
|
||||
allow_origin_string_match:
|
||||
- safe_regex:
|
||||
regex: ".*"
|
||||
allow_methods: "GET,POST,PUT,PATCH,DELETE,OPTIONS,HEAD,CONNECT,TRACE"
|
||||
allow_headers: "*"
|
||||
expose_headers: "*"
|
||||
max_age: "3600"
|
||||
request_headers_to_add:
|
||||
- header:
|
||||
key: X-Forwarded-Host
|
||||
value: "%REQ(:AUTHORITY)%"
|
||||
append_action: ADD_IF_ABSENT
|
||||
- header:
|
||||
key: X-Forwarded-Port
|
||||
value: "%DOWNSTREAM_LOCAL_PORT%"
|
||||
append_action: ADD_IF_ABSENT
|
||||
routes:
|
||||
- match:
|
||||
prefix: /auth/v1/verify
|
||||
route:
|
||||
cluster: auth
|
||||
prefix_rewrite: /verify
|
||||
timeout: 30s
|
||||
request_headers_to_add:
|
||||
- header:
|
||||
key: X-Forwarded-Prefix
|
||||
value: /auth/v1/verify
|
||||
append_action: ADD_IF_ABSENT
|
||||
typed_per_filter_config:
|
||||
envoy.filters.http.basic_auth:
|
||||
'@type': >-
|
||||
type.googleapis.com/envoy.config.route.v3.FilterConfig
|
||||
disabled: true
|
||||
envoy.filters.http.rbac:
|
||||
'@type': >-
|
||||
type.googleapis.com/envoy.extensions.filters.http.rbac.v3.RBACPerRoute
|
||||
rbac:
|
||||
rules:
|
||||
action: ALLOW
|
||||
policies:
|
||||
allow_all:
|
||||
permissions:
|
||||
- any: true
|
||||
principals:
|
||||
- any: true
|
||||
|
||||
- match:
|
||||
prefix: /auth/v1/callback
|
||||
route:
|
||||
cluster: auth
|
||||
prefix_rewrite: /callback
|
||||
timeout: 30s
|
||||
request_headers_to_add:
|
||||
- header:
|
||||
key: X-Forwarded-Prefix
|
||||
value: /auth/v1/callback
|
||||
append_action: ADD_IF_ABSENT
|
||||
typed_per_filter_config:
|
||||
envoy.filters.http.basic_auth:
|
||||
'@type': >-
|
||||
type.googleapis.com/envoy.config.route.v3.FilterConfig
|
||||
disabled: true
|
||||
envoy.filters.http.rbac:
|
||||
'@type': >-
|
||||
type.googleapis.com/envoy.extensions.filters.http.rbac.v3.RBACPerRoute
|
||||
rbac:
|
||||
rules:
|
||||
action: ALLOW
|
||||
policies:
|
||||
allow_all:
|
||||
permissions:
|
||||
- any: true
|
||||
principals:
|
||||
- any: true
|
||||
|
||||
- match:
|
||||
prefix: /auth/v1/authorize
|
||||
route:
|
||||
cluster: auth
|
||||
prefix_rewrite: /authorize
|
||||
timeout: 30s
|
||||
request_headers_to_add:
|
||||
- header:
|
||||
key: X-Forwarded-Prefix
|
||||
value: /auth/v1/authorize
|
||||
append_action: ADD_IF_ABSENT
|
||||
typed_per_filter_config:
|
||||
envoy.filters.http.basic_auth:
|
||||
'@type': >-
|
||||
type.googleapis.com/envoy.config.route.v3.FilterConfig
|
||||
disabled: true
|
||||
envoy.filters.http.rbac:
|
||||
'@type': >-
|
||||
type.googleapis.com/envoy.extensions.filters.http.rbac.v3.RBACPerRoute
|
||||
rbac:
|
||||
rules:
|
||||
action: ALLOW
|
||||
policies:
|
||||
allow_all:
|
||||
permissions:
|
||||
- any: true
|
||||
principals:
|
||||
- any: true
|
||||
|
||||
- match:
|
||||
prefix: /auth/v1/.well-known/jwks.json
|
||||
route:
|
||||
cluster: auth
|
||||
prefix_rewrite: /.well-known/jwks.json
|
||||
timeout: 30s
|
||||
request_headers_to_add:
|
||||
- header:
|
||||
key: X-Forwarded-Prefix
|
||||
value: /auth/v1/.well-known/jwks.json
|
||||
append_action: ADD_IF_ABSENT
|
||||
typed_per_filter_config:
|
||||
envoy.filters.http.basic_auth:
|
||||
'@type': >-
|
||||
type.googleapis.com/envoy.config.route.v3.FilterConfig
|
||||
disabled: true
|
||||
envoy.filters.http.rbac:
|
||||
'@type': >-
|
||||
type.googleapis.com/envoy.extensions.filters.http.rbac.v3.RBACPerRoute
|
||||
rbac:
|
||||
rules:
|
||||
action: ALLOW
|
||||
policies:
|
||||
allow_all:
|
||||
permissions:
|
||||
- any: true
|
||||
principals:
|
||||
- any: true
|
||||
|
||||
- match:
|
||||
prefix: /.well-known/oauth-authorization-server
|
||||
route:
|
||||
cluster: auth
|
||||
timeout: 30s
|
||||
request_headers_to_add:
|
||||
- header:
|
||||
key: X-Forwarded-Prefix
|
||||
value: /.well-known/oauth-authorization-server
|
||||
append_action: ADD_IF_ABSENT
|
||||
typed_per_filter_config:
|
||||
envoy.filters.http.basic_auth:
|
||||
'@type': >-
|
||||
type.googleapis.com/envoy.config.route.v3.FilterConfig
|
||||
disabled: true
|
||||
envoy.filters.http.rbac:
|
||||
'@type': >-
|
||||
type.googleapis.com/envoy.extensions.filters.http.rbac.v3.RBACPerRoute
|
||||
rbac:
|
||||
rules:
|
||||
action: ALLOW
|
||||
policies:
|
||||
allow_all:
|
||||
permissions:
|
||||
- any: true
|
||||
principals:
|
||||
- any: true
|
||||
|
||||
- match:
|
||||
prefix: /sso/saml/acs
|
||||
route:
|
||||
cluster: auth
|
||||
timeout: 30s
|
||||
request_headers_to_add:
|
||||
- header:
|
||||
key: X-Forwarded-Prefix
|
||||
value: /sso/saml/acs
|
||||
append_action: ADD_IF_ABSENT
|
||||
typed_per_filter_config:
|
||||
envoy.filters.http.basic_auth:
|
||||
'@type': >-
|
||||
type.googleapis.com/envoy.config.route.v3.FilterConfig
|
||||
disabled: true
|
||||
envoy.filters.http.rbac:
|
||||
'@type': >-
|
||||
type.googleapis.com/envoy.extensions.filters.http.rbac.v3.RBACPerRoute
|
||||
rbac:
|
||||
rules:
|
||||
action: ALLOW
|
||||
policies:
|
||||
allow_all:
|
||||
permissions:
|
||||
- any: true
|
||||
principals:
|
||||
- any: true
|
||||
|
||||
- match:
|
||||
prefix: /sso/saml/metadata
|
||||
route:
|
||||
cluster: auth
|
||||
timeout: 30s
|
||||
request_headers_to_add:
|
||||
- header:
|
||||
key: X-Forwarded-Prefix
|
||||
value: /sso/saml/metadata
|
||||
append_action: ADD_IF_ABSENT
|
||||
typed_per_filter_config:
|
||||
envoy.filters.http.basic_auth:
|
||||
'@type': >-
|
||||
type.googleapis.com/envoy.config.route.v3.FilterConfig
|
||||
disabled: true
|
||||
envoy.filters.http.rbac:
|
||||
'@type': >-
|
||||
type.googleapis.com/envoy.extensions.filters.http.rbac.v3.RBACPerRoute
|
||||
rbac:
|
||||
rules:
|
||||
action: ALLOW
|
||||
policies:
|
||||
allow_all:
|
||||
permissions:
|
||||
- any: true
|
||||
principals:
|
||||
- any: true
|
||||
|
||||
- name: functions-v1-all
|
||||
match:
|
||||
prefix: /functions/v1/
|
||||
route:
|
||||
cluster: functions
|
||||
prefix_rewrite: /
|
||||
timeout: 150s
|
||||
request_headers_to_add:
|
||||
- header:
|
||||
key: X-Forwarded-Prefix
|
||||
value: /functions/v1/
|
||||
append_action: ADD_IF_ABSENT
|
||||
typed_per_filter_config:
|
||||
envoy.filters.http.basic_auth:
|
||||
'@type': >-
|
||||
type.googleapis.com/envoy.config.route.v3.FilterConfig
|
||||
disabled: true
|
||||
envoy.filters.http.rbac:
|
||||
'@type': >-
|
||||
type.googleapis.com/envoy.extensions.filters.http.rbac.v3.RBACPerRoute
|
||||
rbac:
|
||||
rules:
|
||||
action: ALLOW
|
||||
policies:
|
||||
allow_all:
|
||||
permissions:
|
||||
- any: true
|
||||
principals:
|
||||
- any: true
|
||||
|
||||
- match:
|
||||
prefix: /storage/v1/
|
||||
route:
|
||||
cluster: storage
|
||||
prefix_rewrite: /
|
||||
timeout: 30s
|
||||
request_headers_to_add:
|
||||
- header:
|
||||
key: X-Forwarded-Prefix
|
||||
value: /storage/v1
|
||||
append_action: ADD_IF_ABSENT
|
||||
typed_per_filter_config:
|
||||
envoy.filters.http.basic_auth:
|
||||
'@type': >-
|
||||
type.googleapis.com/envoy.config.route.v3.FilterConfig
|
||||
disabled: true
|
||||
envoy.filters.http.rbac:
|
||||
'@type': >-
|
||||
type.googleapis.com/envoy.extensions.filters.http.rbac.v3.RBACPerRoute
|
||||
rbac:
|
||||
rules:
|
||||
action: ALLOW
|
||||
policies:
|
||||
allow_all:
|
||||
permissions:
|
||||
- any: true
|
||||
principals:
|
||||
- any: true
|
||||
|
||||
- name: auth-v1-protected
|
||||
match:
|
||||
prefix: /auth/v1/
|
||||
route:
|
||||
cluster: auth
|
||||
prefix_rewrite: /
|
||||
timeout: 30s
|
||||
request_headers_to_add:
|
||||
- header:
|
||||
key: X-Forwarded-Prefix
|
||||
value: /auth/v1/
|
||||
append_action: ADD_IF_ABSENT
|
||||
typed_per_filter_config:
|
||||
envoy.filters.http.basic_auth:
|
||||
'@type': >-
|
||||
type.googleapis.com/envoy.config.route.v3.FilterConfig
|
||||
disabled: true
|
||||
|
||||
- name: rest-v1-protected
|
||||
match:
|
||||
prefix: /rest/v1/
|
||||
route:
|
||||
cluster: rest
|
||||
prefix_rewrite: /
|
||||
timeout: 30s
|
||||
request_headers_to_add:
|
||||
- header:
|
||||
key: X-Forwarded-Prefix
|
||||
value: /rest/v1/
|
||||
append_action: ADD_IF_ABSENT
|
||||
typed_per_filter_config:
|
||||
envoy.filters.http.basic_auth:
|
||||
'@type': >-
|
||||
type.googleapis.com/envoy.config.route.v3.FilterConfig
|
||||
disabled: true
|
||||
|
||||
- name: graphql-v1-protected
|
||||
match:
|
||||
prefix: /graphql/v1
|
||||
route:
|
||||
cluster: rest
|
||||
prefix_rewrite: /rpc/graphql
|
||||
timeout: 30s
|
||||
request_headers_to_add:
|
||||
- header:
|
||||
key: X-Forwarded-Prefix
|
||||
value: /graphql/v1
|
||||
append_action: ADD_IF_ABSENT
|
||||
- header:
|
||||
key: Content-Profile
|
||||
value: graphql_public
|
||||
append_action: ADD_IF_ABSENT
|
||||
typed_per_filter_config:
|
||||
envoy.filters.http.basic_auth:
|
||||
'@type': >-
|
||||
type.googleapis.com/envoy.config.route.v3.FilterConfig
|
||||
disabled: true
|
||||
|
||||
- name: realtime-v1-api-protected
|
||||
match:
|
||||
prefix: /realtime/v1/api
|
||||
route:
|
||||
cluster: realtime
|
||||
prefix_rewrite: /api
|
||||
timeout: 30s
|
||||
host_rewrite_literal: realtime-dev.supabase-realtime
|
||||
request_headers_to_add:
|
||||
- header:
|
||||
key: X-Forwarded-Prefix
|
||||
value: /realtime/v1/api
|
||||
append_action: ADD_IF_ABSENT
|
||||
typed_per_filter_config:
|
||||
envoy.filters.http.basic_auth:
|
||||
'@type': >-
|
||||
type.googleapis.com/envoy.config.route.v3.FilterConfig
|
||||
disabled: true
|
||||
|
||||
- name: realtime-v1-ws-protected
|
||||
match:
|
||||
prefix: /realtime/v1/
|
||||
route:
|
||||
cluster: realtime
|
||||
prefix_rewrite: /socket/
|
||||
timeout: 30s
|
||||
host_rewrite_literal: realtime-dev.supabase-realtime
|
||||
request_headers_to_add:
|
||||
- header:
|
||||
key: X-Forwarded-Prefix
|
||||
value: /realtime/v1/
|
||||
append_action: ADD_IF_ABSENT
|
||||
typed_per_filter_config:
|
||||
envoy.filters.http.basic_auth:
|
||||
'@type': >-
|
||||
type.googleapis.com/envoy.config.route.v3.FilterConfig
|
||||
disabled: true
|
||||
|
||||
- name: pg-protected
|
||||
match:
|
||||
prefix: /pg/
|
||||
route:
|
||||
cluster: meta
|
||||
prefix_rewrite: /
|
||||
timeout: 30s
|
||||
request_headers_to_add:
|
||||
- header:
|
||||
key: X-Forwarded-Prefix
|
||||
value: /pg/
|
||||
append_action: ADD_IF_ABSENT
|
||||
typed_per_filter_config:
|
||||
envoy.filters.http.basic_auth:
|
||||
'@type': >-
|
||||
type.googleapis.com/envoy.config.route.v3.FilterConfig
|
||||
disabled: true
|
||||
|
||||
- match:
|
||||
prefix: /api/mcp
|
||||
route:
|
||||
cluster: studio
|
||||
timeout: 30s
|
||||
request_headers_to_add:
|
||||
- header:
|
||||
key: X-Forwarded-Prefix
|
||||
value: /api/mcp
|
||||
append_action: ADD_IF_ABSENT
|
||||
typed_per_filter_config:
|
||||
envoy.filters.http.basic_auth:
|
||||
'@type': >-
|
||||
type.googleapis.com/envoy.config.route.v3.FilterConfig
|
||||
disabled: true
|
||||
envoy.filters.http.rbac:
|
||||
'@type': >-
|
||||
type.googleapis.com/envoy.extensions.filters.http.rbac.v3.RBACPerRoute
|
||||
rbac:
|
||||
rules:
|
||||
action: DENY
|
||||
policies:
|
||||
deny_all:
|
||||
permissions:
|
||||
- any: true
|
||||
principals:
|
||||
- any: true
|
||||
|
||||
- match:
|
||||
prefix: /mcp
|
||||
route:
|
||||
cluster: studio
|
||||
prefix_rewrite: /api/mcp
|
||||
timeout: 30s
|
||||
request_headers_to_add:
|
||||
- header:
|
||||
key: X-Forwarded-Prefix
|
||||
value: /mcp
|
||||
append_action: ADD_IF_ABSENT
|
||||
typed_per_filter_config:
|
||||
envoy.filters.http.basic_auth:
|
||||
'@type': >-
|
||||
type.googleapis.com/envoy.config.route.v3.FilterConfig
|
||||
disabled: true
|
||||
envoy.filters.http.rbac:
|
||||
'@type': >-
|
||||
type.googleapis.com/envoy.extensions.filters.http.rbac.v3.RBACPerRoute
|
||||
# Block access to /mcp by default
|
||||
rbac:
|
||||
rules:
|
||||
action: DENY
|
||||
policies:
|
||||
deny_all:
|
||||
permissions:
|
||||
- any: true
|
||||
principals:
|
||||
- any: true
|
||||
# Enable local access (danger zone!)
|
||||
# 1. Comment out the 'rbac' block above.
|
||||
# 2. Uncomment and adjust the 'rbac' block below.
|
||||
# 3. Add or adjust your local IPs in 'principals'.
|
||||
#rbac:
|
||||
# rules:
|
||||
# action: ALLOW
|
||||
# policies:
|
||||
# allow_local:
|
||||
# permissions:
|
||||
# - any: true
|
||||
# principals:
|
||||
# - direct_remote_ip:
|
||||
# address_prefix: 127.0.0.1
|
||||
# prefix_len: 32
|
||||
# - direct_remote_ip:
|
||||
# address_prefix: ::1
|
||||
# prefix_len: 128
|
||||
|
||||
- match:
|
||||
prefix: /
|
||||
route:
|
||||
cluster: studio
|
||||
timeout: 30s
|
||||
request_headers_to_remove:
|
||||
- authorization
|
||||
request_headers_to_add:
|
||||
- header:
|
||||
key: X-Forwarded-Prefix
|
||||
value: /
|
||||
append_action: ADD_IF_ABSENT
|
||||
typed_per_filter_config:
|
||||
envoy.filters.http.rbac:
|
||||
'@type': >-
|
||||
type.googleapis.com/envoy.extensions.filters.http.rbac.v3.RBACPerRoute
|
||||
rbac:
|
||||
rules:
|
||||
action: ALLOW
|
||||
policies:
|
||||
allow_all:
|
||||
permissions:
|
||||
- any: true
|
||||
principals:
|
||||
- any: true
|
||||
|
||||
http_filters:
|
||||
- name: envoy.filters.http.cors
|
||||
typed_config:
|
||||
'@type': >-
|
||||
type.googleapis.com/envoy.extensions.filters.http.cors.v3.Cors
|
||||
|
||||
- name: envoy.filters.http.basic_auth
|
||||
typed_config:
|
||||
'@type': >-
|
||||
type.googleapis.com/envoy.extensions.filters.http.basic_auth.v3.BasicAuth
|
||||
users:
|
||||
inline_string: '${DASHBOARD_BASIC_AUTH}'
|
||||
|
||||
# Copies ?apikey=... from the URL into the apikey header when clients omit the header.
|
||||
- name: envoy.filters.http.lua
|
||||
typed_config:
|
||||
'@type': >-
|
||||
type.googleapis.com/envoy.extensions.filters.http.lua.v3.Lua
|
||||
inline_code: |
|
||||
local FUNCTIONS_ROUTE = "functions-v1-all"
|
||||
local FUNCTIONS_PREFIX = "/functions/v1/"
|
||||
|
||||
local function is_functions_request(request_handle, headers)
|
||||
if request_handle:streamInfo():routeName() == FUNCTIONS_ROUTE then
|
||||
return true
|
||||
end
|
||||
|
||||
local path = headers:get(":path")
|
||||
if path == nil then
|
||||
return false
|
||||
end
|
||||
|
||||
return string.sub(path, 1, string.len(FUNCTIONS_PREFIX)) == FUNCTIONS_PREFIX
|
||||
end
|
||||
|
||||
function envoy_on_request(request_handle)
|
||||
local headers = request_handle:headers()
|
||||
if is_functions_request(request_handle, headers) then
|
||||
return
|
||||
end
|
||||
|
||||
if headers:get("apikey") ~= nil then
|
||||
return
|
||||
end
|
||||
|
||||
local path = headers:get(":path")
|
||||
local query_start = string.find(path, "?", 1, true)
|
||||
if query_start == nil then
|
||||
return
|
||||
end
|
||||
|
||||
local query = string.sub(path, query_start + 1)
|
||||
for key, value in string.gmatch(query, "([^&]+)=([^&]*)") do
|
||||
if key == "apikey" and value ~= "" then
|
||||
headers:add("apikey", value)
|
||||
return
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
# Returns 401 for missing/invalid API keys on protected API routes.
|
||||
- name: envoy.filters.http.lua
|
||||
typed_config:
|
||||
'@type': >-
|
||||
type.googleapis.com/envoy.extensions.filters.http.lua.v3.Lua
|
||||
inline_code: |
|
||||
local ANON_KEY = "${ANON_KEY}"
|
||||
local SERVICE_ROLE_KEY = "${SERVICE_ROLE_KEY}"
|
||||
local SUPABASE_PUBLISHABLE_KEY = "${SUPABASE_PUBLISHABLE_KEY}"
|
||||
local SUPABASE_SECRET_KEY = "${SUPABASE_SECRET_KEY}"
|
||||
local ANON_KEY_ASYMMETRIC = "${ANON_KEY_ASYMMETRIC}"
|
||||
local SERVICE_ROLE_KEY_ASYMMETRIC = "${SERVICE_ROLE_KEY_ASYMMETRIC}"
|
||||
local TRANSLATION_ENABLED = SUPABASE_SECRET_KEY ~= "" and SUPABASE_PUBLISHABLE_KEY ~= "" and SERVICE_ROLE_KEY_ASYMMETRIC ~= "" and ANON_KEY_ASYMMETRIC ~= ""
|
||||
|
||||
local PROTECTED_ROUTES = {
|
||||
["auth-v1-protected"] = true,
|
||||
["rest-v1-protected"] = true,
|
||||
["graphql-v1-protected"] = true,
|
||||
["realtime-v1-api-protected"] = true,
|
||||
["realtime-v1-ws-protected"] = true,
|
||||
["pg-protected"] = true,
|
||||
}
|
||||
|
||||
local function is_protected_route(route_name)
|
||||
if route_name == nil or route_name == "" then
|
||||
return false
|
||||
end
|
||||
|
||||
return PROTECTED_ROUTES[route_name] == true
|
||||
end
|
||||
|
||||
local function is_valid_apikey(apikey)
|
||||
if apikey == nil or apikey == "" then
|
||||
return false
|
||||
end
|
||||
|
||||
if SERVICE_ROLE_KEY ~= "" and apikey == SERVICE_ROLE_KEY then
|
||||
return true
|
||||
end
|
||||
|
||||
if ANON_KEY ~= "" and apikey == ANON_KEY then
|
||||
return true
|
||||
end
|
||||
|
||||
if TRANSLATION_ENABLED and apikey == SUPABASE_SECRET_KEY then
|
||||
return true
|
||||
end
|
||||
|
||||
if TRANSLATION_ENABLED and apikey == SUPABASE_PUBLISHABLE_KEY then
|
||||
return true
|
||||
end
|
||||
|
||||
return false
|
||||
end
|
||||
|
||||
function envoy_on_request(request_handle)
|
||||
local headers = request_handle:headers()
|
||||
local route_name = request_handle:streamInfo():routeName()
|
||||
if not is_protected_route(route_name) then
|
||||
return
|
||||
end
|
||||
|
||||
if is_valid_apikey(headers:get("apikey")) then
|
||||
return
|
||||
end
|
||||
|
||||
request_handle:respond({
|
||||
[":status"] = "401",
|
||||
["content-type"] = "text/plain",
|
||||
}, "Unauthorized")
|
||||
end
|
||||
|
||||
# Translates the query parameter apikey into the matching internal JWT and rewrites the URL so only JWTs propagate downstream.
|
||||
- name: envoy.filters.http.lua
|
||||
typed_config:
|
||||
'@type': >-
|
||||
type.googleapis.com/envoy.extensions.filters.http.lua.v3.Lua
|
||||
inline_code: |
|
||||
local FUNCTIONS_ROUTE = "functions-v1-all"
|
||||
local FUNCTIONS_PREFIX = "/functions/v1/"
|
||||
local SECRET_KEY = "${SUPABASE_SECRET_KEY}"
|
||||
local PUBLISHABLE_KEY = "${SUPABASE_PUBLISHABLE_KEY}"
|
||||
local SERVICE_ROLE_JWT = "${SERVICE_ROLE_KEY_ASYMMETRIC}"
|
||||
local ANON_JWT = "${ANON_KEY_ASYMMETRIC}"
|
||||
local TRANSLATION_ENABLED = SECRET_KEY ~= "" and PUBLISHABLE_KEY ~= "" and SERVICE_ROLE_JWT ~= "" and ANON_JWT ~= ""
|
||||
|
||||
local function is_functions_request(request_handle, headers)
|
||||
if request_handle:streamInfo():routeName() == FUNCTIONS_ROUTE then
|
||||
return true
|
||||
end
|
||||
|
||||
local path = headers:get(":path")
|
||||
if path == nil then
|
||||
return false
|
||||
end
|
||||
|
||||
return string.sub(path, 1, string.len(FUNCTIONS_PREFIX)) == FUNCTIONS_PREFIX
|
||||
end
|
||||
|
||||
local function translate_apikey(apikey)
|
||||
if apikey == nil or apikey == "" then
|
||||
return nil
|
||||
end
|
||||
|
||||
if not TRANSLATION_ENABLED then
|
||||
return nil
|
||||
end
|
||||
|
||||
if apikey == SECRET_KEY then
|
||||
return SERVICE_ROLE_JWT
|
||||
end
|
||||
|
||||
if apikey == PUBLISHABLE_KEY then
|
||||
return ANON_JWT
|
||||
end
|
||||
|
||||
return nil
|
||||
end
|
||||
|
||||
local function extract_query_apikey(path)
|
||||
if path == nil or path == "" then
|
||||
return nil
|
||||
end
|
||||
|
||||
local query_start = string.find(path, "?", 1, true)
|
||||
if query_start == nil then
|
||||
return nil
|
||||
end
|
||||
|
||||
local query = string.sub(path, query_start + 1)
|
||||
for key, value in string.gmatch(query, "([^&]+)=([^&]*)") do
|
||||
if key == "apikey" and value ~= "" then
|
||||
return value
|
||||
end
|
||||
end
|
||||
|
||||
return nil
|
||||
end
|
||||
|
||||
local function replace_query_apikey(path, new_value)
|
||||
if path == nil or path == "" or new_value == nil or new_value == "" then
|
||||
return nil
|
||||
end
|
||||
|
||||
local query_start = string.find(path, "?", 1, true)
|
||||
if query_start == nil then
|
||||
return nil
|
||||
end
|
||||
|
||||
local base = string.sub(path, 1, query_start)
|
||||
local query = string.sub(path, query_start + 1)
|
||||
local updated = {}
|
||||
local replaced = false
|
||||
|
||||
for part in string.gmatch(query, "([^&]+)") do
|
||||
local key, value = string.match(part, "([^=]+)=(.*)")
|
||||
if key == "apikey" then
|
||||
part = key .. "=" .. new_value
|
||||
replaced = true
|
||||
end
|
||||
table.insert(updated, part)
|
||||
end
|
||||
|
||||
if not replaced then
|
||||
return nil
|
||||
end
|
||||
|
||||
return base .. table.concat(updated, "&")
|
||||
end
|
||||
|
||||
function envoy_on_request(request_handle)
|
||||
local headers = request_handle:headers()
|
||||
if is_functions_request(request_handle, headers) then
|
||||
return
|
||||
end
|
||||
|
||||
local path = headers:get(":path")
|
||||
local apikey = extract_query_apikey(path)
|
||||
local translated = translate_apikey(apikey)
|
||||
|
||||
if translated == nil then
|
||||
return
|
||||
end
|
||||
|
||||
headers:replace("apikey", translated)
|
||||
|
||||
local rewritten_path = replace_query_apikey(path, translated)
|
||||
if rewritten_path ~= nil then
|
||||
headers:replace(":path", rewritten_path)
|
||||
end
|
||||
end
|
||||
|
||||
# Translates an apikey header into the appropriate internal JWT for downstream RBAC checks.
|
||||
- name: envoy.filters.http.lua
|
||||
typed_config:
|
||||
'@type': >-
|
||||
type.googleapis.com/envoy.extensions.filters.http.lua.v3.Lua
|
||||
inline_code: |
|
||||
local FUNCTIONS_ROUTE = "functions-v1-all"
|
||||
local FUNCTIONS_PREFIX = "/functions/v1/"
|
||||
local SECRET_KEY = "${SUPABASE_SECRET_KEY}"
|
||||
local PUBLISHABLE_KEY = "${SUPABASE_PUBLISHABLE_KEY}"
|
||||
local SERVICE_ROLE_JWT = "${SERVICE_ROLE_KEY_ASYMMETRIC}"
|
||||
local ANON_JWT = "${ANON_KEY_ASYMMETRIC}"
|
||||
local TRANSLATION_ENABLED = SECRET_KEY ~= "" and PUBLISHABLE_KEY ~= "" and SERVICE_ROLE_JWT ~= "" and ANON_JWT ~= ""
|
||||
|
||||
local function is_functions_request(request_handle, headers)
|
||||
if request_handle:streamInfo():routeName() == FUNCTIONS_ROUTE then
|
||||
return true
|
||||
end
|
||||
|
||||
local path = headers:get(":path")
|
||||
if path == nil then
|
||||
return false
|
||||
end
|
||||
|
||||
return string.sub(path, 1, string.len(FUNCTIONS_PREFIX)) == FUNCTIONS_PREFIX
|
||||
end
|
||||
|
||||
local function translate_apikey(apikey)
|
||||
if apikey == nil or apikey == "" then
|
||||
return nil
|
||||
end
|
||||
|
||||
if not TRANSLATION_ENABLED then
|
||||
return nil
|
||||
end
|
||||
|
||||
if apikey == SECRET_KEY then
|
||||
return SERVICE_ROLE_JWT
|
||||
end
|
||||
|
||||
if apikey == PUBLISHABLE_KEY then
|
||||
return ANON_JWT
|
||||
end
|
||||
|
||||
return nil
|
||||
end
|
||||
|
||||
function envoy_on_request(request_handle)
|
||||
local headers = request_handle:headers()
|
||||
if is_functions_request(request_handle, headers) then
|
||||
return
|
||||
end
|
||||
|
||||
local translated = translate_apikey(headers:get("apikey"))
|
||||
if translated ~= nil and translated ~= "" then
|
||||
headers:replace("apikey", translated)
|
||||
end
|
||||
end
|
||||
|
||||
# Mirrors apikey into x-api-key for realtime WS compatibility.
|
||||
- name: envoy.filters.http.lua
|
||||
typed_config:
|
||||
'@type': >-
|
||||
type.googleapis.com/envoy.extensions.filters.http.lua.v3.Lua
|
||||
inline_code: |
|
||||
local REALTIME_WS_ROUTE = "realtime-v1-ws-protected"
|
||||
|
||||
function envoy_on_request(request_handle)
|
||||
local route_name = request_handle:streamInfo():routeName()
|
||||
if route_name ~= REALTIME_WS_ROUTE then
|
||||
return
|
||||
end
|
||||
|
||||
local headers = request_handle:headers()
|
||||
local apikey = headers:get("apikey")
|
||||
if apikey == nil or apikey == "" then
|
||||
return
|
||||
end
|
||||
|
||||
headers:replace("x-api-key", apikey)
|
||||
end
|
||||
|
||||
# Synthesizes an Authorization header (Bearer …) from apikey when callers don’t provide a real JWT header.
|
||||
- name: envoy.filters.http.lua
|
||||
typed_config:
|
||||
'@type': >-
|
||||
type.googleapis.com/envoy.extensions.filters.http.lua.v3.Lua
|
||||
inline_code: |
|
||||
local FUNCTIONS_ROUTE = "functions-v1-all"
|
||||
local FUNCTIONS_PREFIX = "/functions/v1/"
|
||||
local REALTIME_WS_ROUTE = "realtime-v1-ws-protected"
|
||||
|
||||
local function is_functions_request(request_handle, headers)
|
||||
if request_handle:streamInfo():routeName() == FUNCTIONS_ROUTE then
|
||||
return true
|
||||
end
|
||||
|
||||
local path = headers:get(":path")
|
||||
if path == nil then
|
||||
return false
|
||||
end
|
||||
|
||||
return string.sub(path, 1, string.len(FUNCTIONS_PREFIX)) == FUNCTIONS_PREFIX
|
||||
end
|
||||
|
||||
local function has_real_jwt(auth_header)
|
||||
if auth_header == nil or auth_header == "" then
|
||||
return false
|
||||
end
|
||||
|
||||
if string.sub(auth_header, 1, 7) ~= "Bearer " then
|
||||
return false
|
||||
end
|
||||
|
||||
return string.sub(auth_header, 1, 10) ~= "Bearer sb_"
|
||||
end
|
||||
|
||||
local function format_authorization(value)
|
||||
if value == nil or value == "" then
|
||||
return nil
|
||||
end
|
||||
|
||||
if string.sub(value, 1, 7) == "Bearer " then
|
||||
return value
|
||||
end
|
||||
|
||||
return "Bearer " .. value
|
||||
end
|
||||
|
||||
function envoy_on_request(request_handle)
|
||||
local headers = request_handle:headers()
|
||||
if request_handle:streamInfo():routeName() == REALTIME_WS_ROUTE then
|
||||
return
|
||||
end
|
||||
|
||||
if is_functions_request(request_handle, headers) then
|
||||
return
|
||||
end
|
||||
|
||||
if has_real_jwt(headers:get("authorization")) then
|
||||
return
|
||||
end
|
||||
|
||||
local apikey = headers:get("apikey")
|
||||
local authorization_value = format_authorization(apikey)
|
||||
if authorization_value ~= nil then
|
||||
headers:replace("authorization", authorization_value)
|
||||
end
|
||||
end
|
||||
|
||||
- name: envoy.filters.http.rbac
|
||||
typed_config:
|
||||
'@type': >-
|
||||
type.googleapis.com/envoy.extensions.filters.http.rbac.v3.RBAC
|
||||
rules:
|
||||
action: ALLOW
|
||||
policies:
|
||||
admin:
|
||||
permissions:
|
||||
- url_path:
|
||||
path:
|
||||
prefix: /pg/
|
||||
principals:
|
||||
- header:
|
||||
name: apikey
|
||||
string_match:
|
||||
exact: '${SERVICE_ROLE_KEY}'
|
||||
- header:
|
||||
name: apikey
|
||||
string_match:
|
||||
exact: '${SERVICE_ROLE_KEY_ASYMMETRIC}'
|
||||
apikey:
|
||||
permissions:
|
||||
- url_path:
|
||||
path:
|
||||
prefix: /auth/v1/
|
||||
- url_path:
|
||||
path:
|
||||
prefix: /rest/v1/
|
||||
- url_path:
|
||||
path:
|
||||
prefix: /realtime/v1/api
|
||||
- url_path:
|
||||
path:
|
||||
prefix: /realtime/v1/
|
||||
- url_path:
|
||||
path:
|
||||
prefix: /graphql/v1
|
||||
principals:
|
||||
- header:
|
||||
name: apikey
|
||||
string_match:
|
||||
exact: '${SERVICE_ROLE_KEY}'
|
||||
- header:
|
||||
name: apikey
|
||||
string_match:
|
||||
exact: '${ANON_KEY}'
|
||||
- header:
|
||||
name: apikey
|
||||
string_match:
|
||||
exact: '${SERVICE_ROLE_KEY_ASYMMETRIC}'
|
||||
- header:
|
||||
name: apikey
|
||||
string_match:
|
||||
exact: '${ANON_KEY_ASYMMETRIC}'
|
||||
- name: envoy.filters.http.router
|
||||
typed_config:
|
||||
'@type': >-
|
||||
type.googleapis.com/envoy.extensions.filters.http.router.v3.Router
|
||||
49
volumes/api/kong-entrypoint.sh
Executable file
49
volumes/api/kong-entrypoint.sh
Executable file
@@ -0,0 +1,49 @@
|
||||
#!/bin/bash
|
||||
# Custom entrypoint for Kong that builds Lua expressions for request-transformer
|
||||
# and performs environment variable substitution in the declarative config.
|
||||
|
||||
# Build Lua expressions for translating opaque API keys to asymmetric JWTs.
|
||||
# When opaque keys are not configured (empty env vars), expressions fall through
|
||||
# to legacy-only behavior - just passing apikey as-is.
|
||||
#
|
||||
# Full expression logic (when opaque keys are configured):
|
||||
# 1. If Authorization header exists and is NOT an sb_ key -> pass through (user session JWT)
|
||||
# 2. If apikey matches secret key -> set service_role asymmetric JWT internal "API key"
|
||||
# 3. If apikey matches publishable key -> set anon asymmetric JWT internal "API key"
|
||||
# 4. Fallback: pass apikey as-is (legacy HS256 JWT)
|
||||
|
||||
if [ -n "$SUPABASE_SECRET_KEY" ] && [ -n "$SUPABASE_PUBLISHABLE_KEY" ]; then
|
||||
# Opaque keys configured -> full translation expressions
|
||||
export LUA_AUTH_EXPR="\$((headers.authorization ~= nil and headers.authorization:sub(1, 10) ~= 'Bearer sb_' and headers.authorization) or (headers.apikey == '$SUPABASE_SECRET_KEY' and 'Bearer $SERVICE_ROLE_KEY_ASYMMETRIC') or (headers.apikey == '$SUPABASE_PUBLISHABLE_KEY' and 'Bearer $ANON_KEY_ASYMMETRIC') or headers.apikey)"
|
||||
|
||||
# Realtime WebSocket: reads from query_params.apikey (supabase-js sends apikey
|
||||
# via query string), outputs to x-api-key header which Realtime checks first.
|
||||
export LUA_RT_WS_EXPR="\$((query_params.apikey == '$SUPABASE_SECRET_KEY' and '$SERVICE_ROLE_KEY_ASYMMETRIC') or (query_params.apikey == '$SUPABASE_PUBLISHABLE_KEY' and '$ANON_KEY_ASYMMETRIC') or query_params.apikey)"
|
||||
else
|
||||
# Legacy API keys, not sb_ API keys -> pass apikey through unchanged
|
||||
export LUA_AUTH_EXPR="\$((headers.authorization ~= nil and headers.authorization:sub(1, 10) ~= 'Bearer sb_' and headers.authorization) or headers.apikey)"
|
||||
export LUA_RT_WS_EXPR="\$(query_params.apikey)"
|
||||
fi
|
||||
|
||||
# Substitute environment variables in the Kong declarative config.
|
||||
# Uses awk instead of eval/echo to preserve YAML quoting (eval strips double
|
||||
# quotes, breaking "Header: value" patterns that YAML parses as mappings).
|
||||
awk '{
|
||||
result = ""
|
||||
rest = $0
|
||||
while (match(rest, /\$[A-Za-z_][A-Za-z_0-9]*/)) {
|
||||
varname = substr(rest, RSTART + 1, RLENGTH - 1)
|
||||
if (varname in ENVIRON) {
|
||||
result = result substr(rest, 1, RSTART - 1) ENVIRON[varname]
|
||||
} else {
|
||||
result = result substr(rest, 1, RSTART + RLENGTH - 1)
|
||||
}
|
||||
rest = substr(rest, RSTART + RLENGTH)
|
||||
}
|
||||
print result rest
|
||||
}' /home/kong/temp.yml > "$KONG_DECLARATIVE_CONFIG"
|
||||
|
||||
# Remove empty key-auth credentials (unconfigured opaque keys)
|
||||
sed -i '/^[[:space:]]*- key:[[:space:]]*$/d' "$KONG_DECLARATIVE_CONFIG"
|
||||
|
||||
exec /entrypoint.sh kong docker-start
|
||||
408
volumes/api/kong.yml
Normal file
408
volumes/api/kong.yml
Normal file
@@ -0,0 +1,408 @@
|
||||
_format_version: '2.1'
|
||||
_transform: true
|
||||
|
||||
###
|
||||
### Consumers / Users
|
||||
###
|
||||
consumers:
|
||||
- username: DASHBOARD
|
||||
- username: anon
|
||||
keyauth_credentials:
|
||||
- key: $SUPABASE_ANON_KEY
|
||||
- key: $SUPABASE_PUBLISHABLE_KEY
|
||||
- username: service_role
|
||||
keyauth_credentials:
|
||||
- key: $SUPABASE_SERVICE_KEY
|
||||
- key: $SUPABASE_SECRET_KEY
|
||||
|
||||
###
|
||||
### Access Control List
|
||||
###
|
||||
acls:
|
||||
- consumer: anon
|
||||
group: anon
|
||||
- consumer: service_role
|
||||
group: admin
|
||||
|
||||
###
|
||||
### Dashboard credentials
|
||||
###
|
||||
basicauth_credentials:
|
||||
- consumer: DASHBOARD
|
||||
username: '$DASHBOARD_USERNAME'
|
||||
password: '$DASHBOARD_PASSWORD'
|
||||
|
||||
###
|
||||
### API Routes
|
||||
###
|
||||
services:
|
||||
## Open Auth routes
|
||||
- name: auth-v1-open
|
||||
_comment: 'Auth: /auth/v1/verify* -> http://auth:9999/verify*'
|
||||
url: http://auth:9999/verify
|
||||
routes:
|
||||
- name: auth-v1-open
|
||||
strip_path: true
|
||||
paths:
|
||||
- /auth/v1/verify
|
||||
plugins:
|
||||
- name: cors
|
||||
- name: auth-v1-open-callback
|
||||
_comment: 'Auth: /auth/v1/callback* -> http://auth:9999/callback*'
|
||||
url: http://auth:9999/callback
|
||||
routes:
|
||||
- name: auth-v1-open-callback
|
||||
strip_path: true
|
||||
paths:
|
||||
- /auth/v1/callback
|
||||
plugins:
|
||||
- name: cors
|
||||
- name: auth-v1-open-authorize
|
||||
_comment: 'Auth: /auth/v1/authorize* -> http://auth:9999/authorize*'
|
||||
url: http://auth:9999/authorize
|
||||
routes:
|
||||
- name: auth-v1-open-authorize
|
||||
strip_path: true
|
||||
paths:
|
||||
- /auth/v1/authorize
|
||||
plugins:
|
||||
- name: cors
|
||||
- name: auth-v1-open-jwks
|
||||
_comment: 'Auth: /auth/v1/.well-known/jwks.json -> http://auth:9999/.well-known/jwks.json'
|
||||
url: http://auth:9999/.well-known/jwks.json
|
||||
routes:
|
||||
- name: auth-v1-open-jwks
|
||||
strip_path: true
|
||||
paths:
|
||||
- /auth/v1/.well-known/jwks.json
|
||||
plugins:
|
||||
- name: cors
|
||||
|
||||
- name: auth-v1-open-sso-acs
|
||||
url: "http://auth:9999/sso/saml/acs"
|
||||
routes:
|
||||
- name: auth-v1-open-sso-acs
|
||||
strip_path: true
|
||||
paths:
|
||||
- /sso/saml/acs
|
||||
plugins:
|
||||
- name: cors
|
||||
|
||||
- name: auth-v1-open-sso-metadata
|
||||
url: "http://auth:9999/sso/saml/metadata"
|
||||
routes:
|
||||
- name: auth-v1-open-sso-metadata
|
||||
strip_path: true
|
||||
paths:
|
||||
- /sso/saml/metadata
|
||||
plugins:
|
||||
- name: cors
|
||||
|
||||
## Secure Auth routes
|
||||
- name: auth-v1
|
||||
_comment: 'Auth: /auth/v1/* -> http://auth:9999/*'
|
||||
url: http://auth:9999/
|
||||
routes:
|
||||
- name: auth-v1-all
|
||||
strip_path: true
|
||||
paths:
|
||||
- /auth/v1/
|
||||
plugins:
|
||||
- name: cors
|
||||
- name: key-auth
|
||||
config:
|
||||
hide_credentials: false
|
||||
- name: request-transformer
|
||||
config:
|
||||
add:
|
||||
headers:
|
||||
- "Authorization: $LUA_AUTH_EXPR"
|
||||
replace:
|
||||
headers:
|
||||
- "Authorization: $LUA_AUTH_EXPR"
|
||||
- name: acl
|
||||
config:
|
||||
hide_groups_header: true
|
||||
allow:
|
||||
- admin
|
||||
- anon
|
||||
|
||||
## Secure PostgREST routes
|
||||
- name: rest-v1
|
||||
_comment: 'PostgREST: /rest/v1/* -> http://rest:3000/*'
|
||||
url: http://rest:3000/
|
||||
routes:
|
||||
- name: rest-v1-all
|
||||
strip_path: true
|
||||
paths:
|
||||
- /rest/v1/
|
||||
plugins:
|
||||
- name: cors
|
||||
- name: key-auth
|
||||
config:
|
||||
hide_credentials: false
|
||||
- name: request-transformer
|
||||
config:
|
||||
add:
|
||||
headers:
|
||||
- "Authorization: $LUA_AUTH_EXPR"
|
||||
replace:
|
||||
headers:
|
||||
- "Authorization: $LUA_AUTH_EXPR"
|
||||
- name: acl
|
||||
config:
|
||||
hide_groups_header: true
|
||||
allow:
|
||||
- admin
|
||||
- anon
|
||||
|
||||
## Secure GraphQL routes
|
||||
- name: graphql-v1
|
||||
_comment: 'PostgREST: /graphql/v1/* -> http://rest:3000/rpc/graphql'
|
||||
url: http://rest:3000/rpc/graphql
|
||||
routes:
|
||||
- name: graphql-v1-all
|
||||
strip_path: true
|
||||
paths:
|
||||
- /graphql/v1
|
||||
plugins:
|
||||
- name: cors
|
||||
- name: key-auth
|
||||
config:
|
||||
hide_credentials: false
|
||||
- name: request-transformer
|
||||
config:
|
||||
add:
|
||||
headers:
|
||||
- "Content-Profile: graphql_public"
|
||||
- "Authorization: $LUA_AUTH_EXPR"
|
||||
replace:
|
||||
headers:
|
||||
- "Authorization: $LUA_AUTH_EXPR"
|
||||
- name: acl
|
||||
config:
|
||||
hide_groups_header: true
|
||||
allow:
|
||||
- admin
|
||||
- anon
|
||||
|
||||
## Secure Realtime routes
|
||||
- name: realtime-v1-ws
|
||||
_comment: 'Realtime: /realtime/v1/* -> ws://realtime:4000/socket/*'
|
||||
url: http://realtime-dev.supabase-realtime:4000/socket
|
||||
protocol: ws
|
||||
routes:
|
||||
- name: realtime-v1-ws
|
||||
strip_path: true
|
||||
paths:
|
||||
- /realtime/v1/
|
||||
plugins:
|
||||
- name: cors
|
||||
- name: key-auth
|
||||
config:
|
||||
hide_credentials: false
|
||||
- name: request-transformer
|
||||
config:
|
||||
add:
|
||||
headers:
|
||||
- "x-api-key:$LUA_RT_WS_EXPR"
|
||||
replace:
|
||||
querystring:
|
||||
- "apikey:$LUA_RT_WS_EXPR"
|
||||
- name: acl
|
||||
config:
|
||||
hide_groups_header: true
|
||||
allow:
|
||||
- admin
|
||||
- anon
|
||||
|
||||
- name: realtime-v1-rest
|
||||
_comment: 'Realtime: /realtime/v1/api/* -> http://realtime:4000/api/*'
|
||||
url: http://realtime-dev.supabase-realtime:4000/api
|
||||
protocol: http
|
||||
routes:
|
||||
- name: realtime-v1-rest
|
||||
strip_path: true
|
||||
paths:
|
||||
- /realtime/v1/api
|
||||
plugins:
|
||||
- name: cors
|
||||
- name: key-auth
|
||||
config:
|
||||
hide_credentials: false
|
||||
- name: request-transformer
|
||||
config:
|
||||
add:
|
||||
headers:
|
||||
- "Authorization: $LUA_AUTH_EXPR"
|
||||
replace:
|
||||
headers:
|
||||
- "Authorization: $LUA_AUTH_EXPR"
|
||||
- name: acl
|
||||
config:
|
||||
hide_groups_header: true
|
||||
allow:
|
||||
- admin
|
||||
- anon
|
||||
|
||||
## Storage API endpoint (with Authorization header transformation).
|
||||
## No key-auth — S3 protocol requests don't carry an apikey header.
|
||||
##
|
||||
## The request-transformer translates opaque API keys to asymmetric JWTs
|
||||
## and passes through existing Authorization headers (user JWTs, AWS SigV4).
|
||||
## When no Authorization or apikey header is present (S3 presigned URLs),
|
||||
## the Lua expression evaluates to nil which Kong renders as empty string.
|
||||
## The post-function strips this empty header so Storage's S3 signature
|
||||
## verification falls through to query-parameter parsing.
|
||||
- name: storage-v1
|
||||
_comment: 'Storage: /storage/v1/* -> http://storage:5000/*'
|
||||
url: http://storage:5000/
|
||||
routes:
|
||||
- name: storage-v1-all
|
||||
strip_path: true
|
||||
paths:
|
||||
- /storage/v1/
|
||||
plugins:
|
||||
- name: cors
|
||||
- name: request-transformer
|
||||
config:
|
||||
add:
|
||||
headers:
|
||||
- "Authorization: $LUA_AUTH_EXPR"
|
||||
replace:
|
||||
headers:
|
||||
- "Authorization: $LUA_AUTH_EXPR"
|
||||
- name: post-function
|
||||
config:
|
||||
access:
|
||||
- |
|
||||
local auth = kong.request.get_header("authorization")
|
||||
if auth == nil or auth == "" or auth:find("^%s*$") then
|
||||
kong.service.request.clear_header("authorization")
|
||||
end
|
||||
|
||||
## Edge Functions routes
|
||||
- name: functions-v1
|
||||
_comment: 'Edge Functions: /functions/v1/* -> http://functions:9000/*'
|
||||
url: http://functions:9000/
|
||||
read_timeout: 150000
|
||||
routes:
|
||||
- name: functions-v1-all
|
||||
strip_path: true
|
||||
paths:
|
||||
- /functions/v1/
|
||||
plugins:
|
||||
- name: cors
|
||||
|
||||
## OAuth 2.0 Authorization Server Metadata (RFC 8414)
|
||||
- name: well-known-oauth
|
||||
_comment: 'Auth: /.well-known/oauth-authorization-server -> http://auth:9999/.well-known/oauth-authorization-server'
|
||||
url: http://auth:9999/.well-known/oauth-authorization-server
|
||||
routes:
|
||||
- name: well-known-oauth
|
||||
strip_path: true
|
||||
paths:
|
||||
- /.well-known/oauth-authorization-server
|
||||
plugins:
|
||||
- name: cors
|
||||
|
||||
## Analytics routes
|
||||
## Not used - Studio and Vector talk directly to analytics via Docker networking.
|
||||
## If external access is needed, add routes with key-auth matching Logflare's x-api-key auth.
|
||||
# - name: analytics-v1-api
|
||||
# _comment: 'Analytics: /analytics/v1/api/endpoints/* -> http://logflare:4000/api/endpoints/*'
|
||||
# url: http://analytics:4000/api/endpoints
|
||||
# routes:
|
||||
# - name: analytics-v1-api
|
||||
# strip_path: true
|
||||
# paths:
|
||||
# - /analytics/v1/api/endpoints/
|
||||
# - name: analytics-v1
|
||||
# _comment: 'Analytics: /analytics/v1/* -> http://logflare:4000/*'
|
||||
# url: http://analytics:4000/
|
||||
# routes:
|
||||
# - name: dashboard-v1-all
|
||||
# strip_path: true
|
||||
# paths:
|
||||
# - /analytics/v1
|
||||
# plugins:
|
||||
# - name: cors
|
||||
# - name: basic-auth
|
||||
# config:
|
||||
# hide_credentials: true
|
||||
|
||||
## Secure Database routes
|
||||
- name: meta
|
||||
_comment: 'pg-meta: /pg/* -> http://pg-meta:8080/*'
|
||||
url: http://meta:8080/
|
||||
routes:
|
||||
- name: meta-all
|
||||
strip_path: true
|
||||
paths:
|
||||
- /pg/
|
||||
plugins:
|
||||
- name: key-auth
|
||||
config:
|
||||
hide_credentials: false
|
||||
- name: acl
|
||||
config:
|
||||
hide_groups_header: true
|
||||
allow:
|
||||
- admin
|
||||
|
||||
## Block access to /api/mcp
|
||||
- name: mcp-blocker
|
||||
_comment: 'Block direct access to /api/mcp'
|
||||
url: http://studio:3000/api/mcp
|
||||
routes:
|
||||
- name: mcp-blocker-route
|
||||
strip_path: true
|
||||
paths:
|
||||
- /api/mcp
|
||||
plugins:
|
||||
- name: request-termination
|
||||
config:
|
||||
status_code: 403
|
||||
message: "Access is forbidden."
|
||||
|
||||
## MCP endpoint - local access
|
||||
- name: mcp
|
||||
_comment: 'MCP: /mcp -> http://studio:3000/api/mcp (local access)'
|
||||
url: http://studio:3000/api/mcp
|
||||
routes:
|
||||
- name: mcp
|
||||
strip_path: true
|
||||
paths:
|
||||
- /mcp
|
||||
plugins:
|
||||
# Block access to /mcp by default
|
||||
- name: request-termination
|
||||
config:
|
||||
status_code: 403
|
||||
message: "Access is forbidden."
|
||||
# Enable local access (danger zone!)
|
||||
# 1. Comment out the 'request-termination' section above
|
||||
# 2. Uncomment the entire section below, including 'deny'
|
||||
# 3. Add your local IPs to the 'allow' list
|
||||
#- name: cors
|
||||
#- name: ip-restriction
|
||||
# config:
|
||||
# allow:
|
||||
# - 127.0.0.1
|
||||
# - ::1
|
||||
# deny: []
|
||||
|
||||
## Protected Dashboard - catch all remaining routes
|
||||
- name: dashboard
|
||||
_comment: 'Studio: /* -> http://studio:3000/*'
|
||||
url: http://studio:3000/
|
||||
routes:
|
||||
- name: dashboard-all
|
||||
strip_path: true
|
||||
paths:
|
||||
- /
|
||||
plugins:
|
||||
- name: cors
|
||||
- name: basic-auth
|
||||
config:
|
||||
hide_credentials: true
|
||||
3
volumes/db/_supabase.sql
Normal file
3
volumes/db/_supabase.sql
Normal file
@@ -0,0 +1,3 @@
|
||||
\set pguser `echo "$POSTGRES_USER"`
|
||||
|
||||
CREATE DATABASE _supabase WITH OWNER :pguser;
|
||||
0
volumes/db/init/data.sql
Executable file
0
volumes/db/init/data.sql
Executable file
5
volumes/db/jwt.sql
Normal file
5
volumes/db/jwt.sql
Normal file
@@ -0,0 +1,5 @@
|
||||
\set jwt_secret `echo "$JWT_SECRET"`
|
||||
\set jwt_exp `echo "$JWT_EXP"`
|
||||
|
||||
ALTER DATABASE postgres SET "app.settings.jwt_secret" TO :'jwt_secret';
|
||||
ALTER DATABASE postgres SET "app.settings.jwt_exp" TO :'jwt_exp';
|
||||
6
volumes/db/logs.sql
Normal file
6
volumes/db/logs.sql
Normal file
@@ -0,0 +1,6 @@
|
||||
\set pguser `echo "$POSTGRES_USER"`
|
||||
|
||||
\c _supabase
|
||||
create schema if not exists _analytics;
|
||||
alter schema _analytics owner to :pguser;
|
||||
\c postgres
|
||||
6
volumes/db/pooler.sql
Normal file
6
volumes/db/pooler.sql
Normal file
@@ -0,0 +1,6 @@
|
||||
\set pguser `echo "$POSTGRES_USER"`
|
||||
|
||||
\c _supabase
|
||||
create schema if not exists _supavisor;
|
||||
alter schema _supavisor owner to :pguser;
|
||||
\c postgres
|
||||
4
volumes/db/realtime.sql
Normal file
4
volumes/db/realtime.sql
Normal file
@@ -0,0 +1,4 @@
|
||||
\set pguser `echo "$POSTGRES_USER"`
|
||||
|
||||
create schema if not exists _realtime;
|
||||
alter schema _realtime owner to :pguser;
|
||||
8
volumes/db/roles.sql
Normal file
8
volumes/db/roles.sql
Normal file
@@ -0,0 +1,8 @@
|
||||
-- NOTE: change to your own passwords for production environments
|
||||
\set pgpass `echo "$POSTGRES_PASSWORD"`
|
||||
|
||||
ALTER USER authenticator WITH PASSWORD :'pgpass';
|
||||
ALTER USER pgbouncer WITH PASSWORD :'pgpass';
|
||||
ALTER USER supabase_auth_admin WITH PASSWORD :'pgpass';
|
||||
ALTER USER supabase_functions_admin WITH PASSWORD :'pgpass';
|
||||
ALTER USER supabase_storage_admin WITH PASSWORD :'pgpass';
|
||||
208
volumes/db/webhooks.sql
Normal file
208
volumes/db/webhooks.sql
Normal file
@@ -0,0 +1,208 @@
|
||||
BEGIN;
|
||||
-- Create pg_net extension
|
||||
CREATE EXTENSION IF NOT EXISTS pg_net SCHEMA extensions;
|
||||
-- Create supabase_functions schema
|
||||
CREATE SCHEMA supabase_functions AUTHORIZATION supabase_admin;
|
||||
GRANT USAGE ON SCHEMA supabase_functions TO postgres, anon, authenticated, service_role;
|
||||
ALTER DEFAULT PRIVILEGES IN SCHEMA supabase_functions GRANT ALL ON TABLES TO postgres, anon, authenticated, service_role;
|
||||
ALTER DEFAULT PRIVILEGES IN SCHEMA supabase_functions GRANT ALL ON FUNCTIONS TO postgres, anon, authenticated, service_role;
|
||||
ALTER DEFAULT PRIVILEGES IN SCHEMA supabase_functions GRANT ALL ON SEQUENCES TO postgres, anon, authenticated, service_role;
|
||||
-- supabase_functions.migrations definition
|
||||
CREATE TABLE supabase_functions.migrations (
|
||||
version text PRIMARY KEY,
|
||||
inserted_at timestamptz NOT NULL DEFAULT NOW()
|
||||
);
|
||||
-- Initial supabase_functions migration
|
||||
INSERT INTO supabase_functions.migrations (version) VALUES ('initial');
|
||||
-- supabase_functions.hooks definition
|
||||
CREATE TABLE supabase_functions.hooks (
|
||||
id bigserial PRIMARY KEY,
|
||||
hook_table_id integer NOT NULL,
|
||||
hook_name text NOT NULL,
|
||||
created_at timestamptz NOT NULL DEFAULT NOW(),
|
||||
request_id bigint
|
||||
);
|
||||
CREATE INDEX supabase_functions_hooks_request_id_idx ON supabase_functions.hooks USING btree (request_id);
|
||||
CREATE INDEX supabase_functions_hooks_h_table_id_h_name_idx ON supabase_functions.hooks USING btree (hook_table_id, hook_name);
|
||||
COMMENT ON TABLE supabase_functions.hooks IS 'Supabase Functions Hooks: Audit trail for triggered hooks.';
|
||||
CREATE FUNCTION supabase_functions.http_request()
|
||||
RETURNS trigger
|
||||
LANGUAGE plpgsql
|
||||
AS $function$
|
||||
DECLARE
|
||||
request_id bigint;
|
||||
payload jsonb;
|
||||
url text := TG_ARGV[0]::text;
|
||||
method text := TG_ARGV[1]::text;
|
||||
headers jsonb DEFAULT '{}'::jsonb;
|
||||
params jsonb DEFAULT '{}'::jsonb;
|
||||
timeout_ms integer DEFAULT 1000;
|
||||
BEGIN
|
||||
IF url IS NULL OR url = 'null' THEN
|
||||
RAISE EXCEPTION 'url argument is missing';
|
||||
END IF;
|
||||
|
||||
IF method IS NULL OR method = 'null' THEN
|
||||
RAISE EXCEPTION 'method argument is missing';
|
||||
END IF;
|
||||
|
||||
IF TG_ARGV[2] IS NULL OR TG_ARGV[2] = 'null' THEN
|
||||
headers = '{"Content-Type": "application/json"}'::jsonb;
|
||||
ELSE
|
||||
headers = TG_ARGV[2]::jsonb;
|
||||
END IF;
|
||||
|
||||
IF TG_ARGV[3] IS NULL OR TG_ARGV[3] = 'null' THEN
|
||||
params = '{}'::jsonb;
|
||||
ELSE
|
||||
params = TG_ARGV[3]::jsonb;
|
||||
END IF;
|
||||
|
||||
IF TG_ARGV[4] IS NULL OR TG_ARGV[4] = 'null' THEN
|
||||
timeout_ms = 1000;
|
||||
ELSE
|
||||
timeout_ms = TG_ARGV[4]::integer;
|
||||
END IF;
|
||||
|
||||
CASE
|
||||
WHEN method = 'GET' THEN
|
||||
SELECT http_get INTO request_id FROM net.http_get(
|
||||
url,
|
||||
params,
|
||||
headers,
|
||||
timeout_ms
|
||||
);
|
||||
WHEN method = 'POST' THEN
|
||||
payload = jsonb_build_object(
|
||||
'old_record', OLD,
|
||||
'record', NEW,
|
||||
'type', TG_OP,
|
||||
'table', TG_TABLE_NAME,
|
||||
'schema', TG_TABLE_SCHEMA
|
||||
);
|
||||
|
||||
SELECT http_post INTO request_id FROM net.http_post(
|
||||
url,
|
||||
payload,
|
||||
params,
|
||||
headers,
|
||||
timeout_ms
|
||||
);
|
||||
ELSE
|
||||
RAISE EXCEPTION 'method argument % is invalid', method;
|
||||
END CASE;
|
||||
|
||||
INSERT INTO supabase_functions.hooks
|
||||
(hook_table_id, hook_name, request_id)
|
||||
VALUES
|
||||
(TG_RELID, TG_NAME, request_id);
|
||||
|
||||
RETURN NEW;
|
||||
END
|
||||
$function$;
|
||||
-- Supabase super admin
|
||||
DO
|
||||
$$
|
||||
BEGIN
|
||||
IF NOT EXISTS (
|
||||
SELECT 1
|
||||
FROM pg_roles
|
||||
WHERE rolname = 'supabase_functions_admin'
|
||||
)
|
||||
THEN
|
||||
CREATE USER supabase_functions_admin NOINHERIT CREATEROLE LOGIN NOREPLICATION;
|
||||
END IF;
|
||||
END
|
||||
$$;
|
||||
GRANT ALL PRIVILEGES ON SCHEMA supabase_functions TO supabase_functions_admin;
|
||||
GRANT ALL PRIVILEGES ON ALL TABLES IN SCHEMA supabase_functions TO supabase_functions_admin;
|
||||
GRANT ALL PRIVILEGES ON ALL SEQUENCES IN SCHEMA supabase_functions TO supabase_functions_admin;
|
||||
ALTER USER supabase_functions_admin SET search_path = "supabase_functions";
|
||||
ALTER table "supabase_functions".migrations OWNER TO supabase_functions_admin;
|
||||
ALTER table "supabase_functions".hooks OWNER TO supabase_functions_admin;
|
||||
ALTER function "supabase_functions".http_request() OWNER TO supabase_functions_admin;
|
||||
GRANT supabase_functions_admin TO postgres;
|
||||
-- Remove unused supabase_pg_net_admin role
|
||||
DO
|
||||
$$
|
||||
BEGIN
|
||||
IF EXISTS (
|
||||
SELECT 1
|
||||
FROM pg_roles
|
||||
WHERE rolname = 'supabase_pg_net_admin'
|
||||
)
|
||||
THEN
|
||||
REASSIGN OWNED BY supabase_pg_net_admin TO supabase_admin;
|
||||
DROP OWNED BY supabase_pg_net_admin;
|
||||
DROP ROLE supabase_pg_net_admin;
|
||||
END IF;
|
||||
END
|
||||
$$;
|
||||
-- pg_net grants when extension is already enabled
|
||||
DO
|
||||
$$
|
||||
BEGIN
|
||||
IF EXISTS (
|
||||
SELECT 1
|
||||
FROM pg_extension
|
||||
WHERE extname = 'pg_net'
|
||||
)
|
||||
THEN
|
||||
GRANT USAGE ON SCHEMA net TO supabase_functions_admin, postgres, anon, authenticated, service_role;
|
||||
ALTER function net.http_get(url text, params jsonb, headers jsonb, timeout_milliseconds integer) SECURITY DEFINER;
|
||||
ALTER function net.http_post(url text, body jsonb, params jsonb, headers jsonb, timeout_milliseconds integer) SECURITY DEFINER;
|
||||
ALTER function net.http_get(url text, params jsonb, headers jsonb, timeout_milliseconds integer) SET search_path = net;
|
||||
ALTER function net.http_post(url text, body jsonb, params jsonb, headers jsonb, timeout_milliseconds integer) SET search_path = net;
|
||||
REVOKE ALL ON FUNCTION net.http_get(url text, params jsonb, headers jsonb, timeout_milliseconds integer) FROM PUBLIC;
|
||||
REVOKE ALL ON FUNCTION net.http_post(url text, body jsonb, params jsonb, headers jsonb, timeout_milliseconds integer) FROM PUBLIC;
|
||||
GRANT EXECUTE ON FUNCTION net.http_get(url text, params jsonb, headers jsonb, timeout_milliseconds integer) TO supabase_functions_admin, postgres, anon, authenticated, service_role;
|
||||
GRANT EXECUTE ON FUNCTION net.http_post(url text, body jsonb, params jsonb, headers jsonb, timeout_milliseconds integer) TO supabase_functions_admin, postgres, anon, authenticated, service_role;
|
||||
END IF;
|
||||
END
|
||||
$$;
|
||||
-- Event trigger for pg_net
|
||||
CREATE OR REPLACE FUNCTION extensions.grant_pg_net_access()
|
||||
RETURNS event_trigger
|
||||
LANGUAGE plpgsql
|
||||
AS $$
|
||||
BEGIN
|
||||
IF EXISTS (
|
||||
SELECT 1
|
||||
FROM pg_event_trigger_ddl_commands() AS ev
|
||||
JOIN pg_extension AS ext
|
||||
ON ev.objid = ext.oid
|
||||
WHERE ext.extname = 'pg_net'
|
||||
)
|
||||
THEN
|
||||
GRANT USAGE ON SCHEMA net TO supabase_functions_admin, postgres, anon, authenticated, service_role;
|
||||
ALTER function net.http_get(url text, params jsonb, headers jsonb, timeout_milliseconds integer) SECURITY DEFINER;
|
||||
ALTER function net.http_post(url text, body jsonb, params jsonb, headers jsonb, timeout_milliseconds integer) SECURITY DEFINER;
|
||||
ALTER function net.http_get(url text, params jsonb, headers jsonb, timeout_milliseconds integer) SET search_path = net;
|
||||
ALTER function net.http_post(url text, body jsonb, params jsonb, headers jsonb, timeout_milliseconds integer) SET search_path = net;
|
||||
REVOKE ALL ON FUNCTION net.http_get(url text, params jsonb, headers jsonb, timeout_milliseconds integer) FROM PUBLIC;
|
||||
REVOKE ALL ON FUNCTION net.http_post(url text, body jsonb, params jsonb, headers jsonb, timeout_milliseconds integer) FROM PUBLIC;
|
||||
GRANT EXECUTE ON FUNCTION net.http_get(url text, params jsonb, headers jsonb, timeout_milliseconds integer) TO supabase_functions_admin, postgres, anon, authenticated, service_role;
|
||||
GRANT EXECUTE ON FUNCTION net.http_post(url text, body jsonb, params jsonb, headers jsonb, timeout_milliseconds integer) TO supabase_functions_admin, postgres, anon, authenticated, service_role;
|
||||
END IF;
|
||||
END;
|
||||
$$;
|
||||
COMMENT ON FUNCTION extensions.grant_pg_net_access IS 'Grants access to pg_net';
|
||||
DO
|
||||
$$
|
||||
BEGIN
|
||||
IF NOT EXISTS (
|
||||
SELECT 1
|
||||
FROM pg_event_trigger
|
||||
WHERE evtname = 'issue_pg_net_access'
|
||||
) THEN
|
||||
CREATE EVENT TRIGGER issue_pg_net_access ON ddl_command_end WHEN TAG IN ('CREATE EXTENSION')
|
||||
EXECUTE PROCEDURE extensions.grant_pg_net_access();
|
||||
END IF;
|
||||
END
|
||||
$$;
|
||||
INSERT INTO supabase_functions.migrations (version) VALUES ('20210809183423_update_grants');
|
||||
ALTER function supabase_functions.http_request() SECURITY DEFINER;
|
||||
ALTER function supabase_functions.http_request() SET search_path = supabase_functions;
|
||||
REVOKE ALL ON FUNCTION supabase_functions.http_request() FROM PUBLIC;
|
||||
GRANT EXECUTE ON FUNCTION supabase_functions.http_request() TO postgres, anon, authenticated, service_role;
|
||||
COMMIT;
|
||||
14
volumes/functions/hello/index.ts
Normal file
14
volumes/functions/hello/index.ts
Normal file
@@ -0,0 +1,14 @@
|
||||
// Follow this setup guide to integrate the Deno language server with your editor:
|
||||
// https://deno.land/manual/getting_started/setup_your_environment
|
||||
// This enables autocomplete, go to definition, etc.
|
||||
|
||||
Deno.serve(async () => {
|
||||
return new Response(
|
||||
`"Hello from Edge Functions!"`,
|
||||
{ headers: { "Content-Type": "application/json" } },
|
||||
)
|
||||
})
|
||||
|
||||
// To invoke:
|
||||
// curl 'http://localhost:<KONG_HTTP_PORT>/functions/v1/hello' \
|
||||
// --header 'Authorization: Bearer <anon/service_role API key>'
|
||||
168
volumes/functions/main/index.ts
Normal file
168
volumes/functions/main/index.ts
Normal file
@@ -0,0 +1,168 @@
|
||||
import * as jose from 'https://deno.land/x/jose@v4.14.4/index.ts'
|
||||
|
||||
console.log('main function started')
|
||||
|
||||
const JWT_SECRET = Deno.env.get('JWT_SECRET')
|
||||
const SUPABASE_URL = Deno.env.get('SUPABASE_URL')
|
||||
const VERIFY_JWT = Deno.env.get('VERIFY_JWT') === 'true'
|
||||
|
||||
// Create JWKS for ES256/RS256 tokens (newer tokens)
|
||||
let SUPABASE_JWT_KEYS: ReturnType<typeof jose.createRemoteJWKSet> | null = null
|
||||
if (SUPABASE_URL) {
|
||||
try {
|
||||
SUPABASE_JWT_KEYS = jose.createRemoteJWKSet(
|
||||
new URL('/auth/v1/.well-known/jwks.json', SUPABASE_URL)
|
||||
)
|
||||
} catch (e) {
|
||||
console.error('Failed to fetch JWKS from SUPABASE_URL:', e)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract JWT token from Authorization header
|
||||
*
|
||||
* Parses the Authorization header to extract the Bearer token.
|
||||
* Expects format: "Bearer <token>"
|
||||
*
|
||||
* @param req - The HTTP request object
|
||||
* @returns The JWT token string
|
||||
* @throws Error if Authorization header is missing or malformed
|
||||
*/
|
||||
function getAuthToken(req: Request) {
|
||||
const authHeader = req.headers.get('authorization')
|
||||
if (!authHeader) {
|
||||
throw new Error('Missing authorization header')
|
||||
}
|
||||
const [bearer, token] = authHeader.split(' ')
|
||||
if (bearer !== 'Bearer') {
|
||||
throw new Error(`Auth header is not 'Bearer {token}'`)
|
||||
}
|
||||
return token
|
||||
}
|
||||
|
||||
async function isValidLegacyJWT(jwt: string): Promise<boolean> {
|
||||
if (!JWT_SECRET) {
|
||||
console.error('JWT_SECRET not available for HS256 token verification')
|
||||
return false
|
||||
}
|
||||
|
||||
const encoder = new TextEncoder();
|
||||
const secretKey = encoder.encode(JWT_SECRET)
|
||||
|
||||
try {
|
||||
await jose.jwtVerify(jwt, secretKey);
|
||||
} catch (e) {
|
||||
console.error('Symmetric Legacy JWT verification error', e);
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
async function isValidJWT(jwt: string): Promise<boolean> {
|
||||
if (!SUPABASE_JWT_KEYS) {
|
||||
console.error('JWKS not available for ES256/RS256 token verification')
|
||||
return false
|
||||
}
|
||||
|
||||
try {
|
||||
await jose.jwtVerify(jwt, SUPABASE_JWT_KEYS)
|
||||
} catch (e) {
|
||||
console.error('Asymmetric JWT verification error', e);
|
||||
return false
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Verify JWT token, handling both legacy (HS256) and newer (ES256/RS256) algorithms
|
||||
*
|
||||
* This function automatically detects the algorithm used in the token and applies
|
||||
* the appropriate verification method:
|
||||
* - HS256: Uses JWT_SECRET (symmetric key)
|
||||
* - ES256/RS256: Uses JWKS endpoint (asymmetric public keys)
|
||||
*
|
||||
* This fix ensures compatibility with both legacy tokens and newer asymmetric tokens,
|
||||
* resolving the "Key for the ES256 algorithm must be of type CryptoKey" error.
|
||||
*
|
||||
* @param jwt - The JWT token string to verify
|
||||
* @returns Promise resolving to true if verification succeeds, false otherwise
|
||||
*/
|
||||
async function isValidHybridJWT(jwt: string): Promise<boolean> {
|
||||
const { alg: jwtAlgorithm } = jose.decodeProtectedHeader(jwt)
|
||||
|
||||
if (jwtAlgorithm === 'HS256') {
|
||||
console.log(`Legacy token type detected, attempting ${jwtAlgorithm} verification.`)
|
||||
|
||||
return await isValidLegacyJWT(jwt)
|
||||
}
|
||||
|
||||
if (jwtAlgorithm === 'ES256' || jwtAlgorithm === 'RS256') {
|
||||
return await isValidJWT(jwt)
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
Deno.serve(async (req: Request) => {
|
||||
if (req.method !== 'OPTIONS' && VERIFY_JWT) {
|
||||
try {
|
||||
const token = getAuthToken(req)
|
||||
const isValidJWT = await isValidHybridJWT(token);
|
||||
|
||||
if (!isValidJWT) {
|
||||
return new Response(JSON.stringify({ msg: 'Invalid JWT' }), {
|
||||
status: 401,
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
})
|
||||
}
|
||||
} catch (e) {
|
||||
console.error(e)
|
||||
return new Response(JSON.stringify({ msg: e.toString() }), {
|
||||
status: 401,
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
const url = new URL(req.url)
|
||||
const { pathname } = url
|
||||
const path_parts = pathname.split('/')
|
||||
const service_name = path_parts[1]
|
||||
|
||||
if (!service_name || service_name === '') {
|
||||
const error = { msg: 'missing function name in request' }
|
||||
return new Response(JSON.stringify(error), {
|
||||
status: 400,
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
})
|
||||
}
|
||||
|
||||
const servicePath = `/home/deno/functions/${service_name}`
|
||||
console.error(`serving the request with ${servicePath}`)
|
||||
|
||||
const memoryLimitMb = 150
|
||||
const workerTimeoutMs = 1 * 60 * 1000
|
||||
const noModuleCache = false
|
||||
const importMapPath = null
|
||||
const envVarsObj = Deno.env.toObject()
|
||||
const envVars = Object.keys(envVarsObj).map((k) => [k, envVarsObj[k]])
|
||||
|
||||
try {
|
||||
const worker = await EdgeRuntime.userWorkers.create({
|
||||
servicePath,
|
||||
memoryLimitMb,
|
||||
workerTimeoutMs,
|
||||
noModuleCache,
|
||||
importMapPath,
|
||||
envVars,
|
||||
})
|
||||
return await worker.fetch(req)
|
||||
} catch (e) {
|
||||
const error = { msg: e.toString() }
|
||||
return new Response(JSON.stringify(error), {
|
||||
status: 500,
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
})
|
||||
}
|
||||
})
|
||||
268
volumes/logs/vector.yml
Normal file
268
volumes/logs/vector.yml
Normal file
@@ -0,0 +1,268 @@
|
||||
api:
|
||||
enabled: true
|
||||
address: 0.0.0.0:9001
|
||||
|
||||
sources:
|
||||
docker_host:
|
||||
type: docker_logs
|
||||
exclude_containers:
|
||||
- supabase-vector
|
||||
|
||||
transforms:
|
||||
project_logs:
|
||||
type: remap
|
||||
inputs:
|
||||
- docker_host
|
||||
source: |-
|
||||
.project = "default"
|
||||
.event_message = del(.message)
|
||||
.appname = del(.container_name)
|
||||
del(.container_created_at)
|
||||
del(.container_id)
|
||||
del(.source_type)
|
||||
del(.stream)
|
||||
del(.label)
|
||||
del(.image)
|
||||
del(.host)
|
||||
del(.stream)
|
||||
router:
|
||||
type: route
|
||||
inputs:
|
||||
- project_logs
|
||||
route:
|
||||
kong: '.appname == "supabase-kong" || .appname == "supabase-envoy"'
|
||||
auth: '.appname == "supabase-auth"'
|
||||
rest: '.appname == "supabase-rest"'
|
||||
realtime: '.appname == "realtime-dev.supabase-realtime"'
|
||||
storage: '.appname == "supabase-storage"'
|
||||
functions: '.appname == "supabase-edge-functions"'
|
||||
db: '.appname == "supabase-db"'
|
||||
# Ignores non nginx errors since they are related with kong booting up
|
||||
kong_logs:
|
||||
type: remap
|
||||
inputs:
|
||||
- router.kong
|
||||
source: |-
|
||||
req, err = parse_nginx_log(.event_message, "combined")
|
||||
if err == null {
|
||||
.timestamp = req.timestamp
|
||||
.metadata.request.headers.referer = req.referer
|
||||
.metadata.request.headers.user_agent = req.agent
|
||||
.metadata.request.headers.cf_connecting_ip = req.client
|
||||
.metadata.response.status_code = req.status
|
||||
url, split_err = split(req.request, " ")
|
||||
if split_err == null {
|
||||
.metadata.request.method = url[0]
|
||||
.metadata.request.path = url[1]
|
||||
.metadata.request.protocol = url[2]
|
||||
}
|
||||
}
|
||||
if err != null {
|
||||
abort
|
||||
}
|
||||
# Ignores non nginx errors since they are related with kong booting up
|
||||
kong_err:
|
||||
type: remap
|
||||
inputs:
|
||||
- router.kong
|
||||
source: |-
|
||||
.metadata.request.method = "GET"
|
||||
.metadata.response.status_code = 200
|
||||
parsed, err = parse_nginx_log(.event_message, "error")
|
||||
if err == null {
|
||||
.timestamp = parsed.timestamp
|
||||
.severity = parsed.severity
|
||||
.metadata.request.host = parsed.host
|
||||
.metadata.request.headers.cf_connecting_ip = parsed.client
|
||||
url, err = split(parsed.request, " ")
|
||||
if err == null {
|
||||
.metadata.request.method = url[0]
|
||||
.metadata.request.path = url[1]
|
||||
.metadata.request.protocol = url[2]
|
||||
}
|
||||
}
|
||||
if err != null {
|
||||
abort
|
||||
}
|
||||
# Gotrue logs are structured json strings which frontend parses directly. But we keep metadata for consistency.
|
||||
auth_logs:
|
||||
type: remap
|
||||
inputs:
|
||||
- router.auth
|
||||
source: |-
|
||||
parsed, err = parse_json(.event_message)
|
||||
if err == null {
|
||||
.metadata.timestamp = parsed.time
|
||||
.metadata = merge!(.metadata, parsed)
|
||||
}
|
||||
# PostgREST logs are structured so we separate timestamp from message using regex
|
||||
rest_logs:
|
||||
type: remap
|
||||
inputs:
|
||||
- router.rest
|
||||
source: |-
|
||||
parsed, err = parse_regex(.event_message, r'^(?P<time>.*): (?P<msg>.*)$')
|
||||
if err == null {
|
||||
.event_message = parsed.msg
|
||||
.timestamp = parse_timestamp!(value: parsed.time,format: "%d/%b/%Y:%H:%M:%S %z")
|
||||
.metadata.host = .project
|
||||
}
|
||||
# Filter out healthcheck logs from Realtime
|
||||
realtime_logs_filtered:
|
||||
type: filter
|
||||
inputs:
|
||||
- router.realtime
|
||||
condition: '!contains(string!(.event_message), "/health")'
|
||||
# Realtime logs are structured so we parse the severity level using regex (ignore time because it has no date)
|
||||
realtime_logs:
|
||||
type: remap
|
||||
inputs:
|
||||
- realtime_logs_filtered
|
||||
source: |-
|
||||
.metadata.project = del(.project)
|
||||
.metadata.external_id = .metadata.project
|
||||
parsed, err = parse_regex(.event_message, r'^(?P<time>\d+:\d+:\d+\.\d+) \[(?P<level>\w+)\] (?P<msg>.*)$')
|
||||
if err == null {
|
||||
.event_message = parsed.msg
|
||||
.metadata.level = parsed.level
|
||||
}
|
||||
# Function logs are unstructured messages on stderr
|
||||
functions_logs:
|
||||
type: remap
|
||||
inputs:
|
||||
- router.functions
|
||||
source: |-
|
||||
.metadata.project_ref = del(.project)
|
||||
# Storage logs may contain json objects so we parse them for completeness
|
||||
storage_logs:
|
||||
type: remap
|
||||
inputs:
|
||||
- router.storage
|
||||
source: |-
|
||||
.metadata.project = del(.project)
|
||||
.metadata.tenantId = .metadata.project
|
||||
parsed, err = parse_json(.event_message)
|
||||
if err == null {
|
||||
.event_message = parsed.msg
|
||||
.metadata.level = parsed.level
|
||||
.metadata.timestamp = parsed.time
|
||||
.metadata.context[0].host = parsed.hostname
|
||||
.metadata.context[0].pid = parsed.pid
|
||||
}
|
||||
# Postgres logs some messages to stderr which we map to warning severity level
|
||||
db_logs:
|
||||
type: remap
|
||||
inputs:
|
||||
- router.db
|
||||
source: |-
|
||||
.metadata.host = "db-default"
|
||||
.metadata.parsed.timestamp = .timestamp
|
||||
|
||||
parsed, err = parse_regex(.event_message, r'.*(?P<level>INFO|NOTICE|WARNING|ERROR|LOG|FATAL|PANIC?):.*', numeric_groups: true)
|
||||
|
||||
if err != null || parsed == null {
|
||||
.metadata.parsed.error_severity = "info"
|
||||
}
|
||||
if parsed.level != null {
|
||||
.metadata.parsed.error_severity = parsed.level
|
||||
}
|
||||
if .metadata.parsed.error_severity == "info" {
|
||||
.metadata.parsed.error_severity = "log"
|
||||
}
|
||||
.metadata.parsed.error_severity = upcase!(.metadata.parsed.error_severity)
|
||||
|
||||
sinks:
|
||||
logflare_auth:
|
||||
type: 'http'
|
||||
inputs:
|
||||
- auth_logs
|
||||
encoding:
|
||||
codec: 'json'
|
||||
method: 'post'
|
||||
request:
|
||||
retry_max_duration_secs: 30
|
||||
retry_initial_backoff_secs: 1
|
||||
headers:
|
||||
x-api-key: ${LOGFLARE_PUBLIC_ACCESS_TOKEN?LOGFLARE_PUBLIC_ACCESS_TOKEN is required}
|
||||
uri: 'http://analytics:4000/api/logs?source_name=gotrue.logs.prod'
|
||||
logflare_realtime:
|
||||
type: 'http'
|
||||
inputs:
|
||||
- realtime_logs
|
||||
encoding:
|
||||
codec: 'json'
|
||||
method: 'post'
|
||||
request:
|
||||
retry_max_duration_secs: 30
|
||||
retry_initial_backoff_secs: 1
|
||||
headers:
|
||||
x-api-key: ${LOGFLARE_PUBLIC_ACCESS_TOKEN?LOGFLARE_PUBLIC_ACCESS_TOKEN is required}
|
||||
uri: 'http://analytics:4000/api/logs?source_name=realtime.logs.prod'
|
||||
logflare_rest:
|
||||
type: 'http'
|
||||
inputs:
|
||||
- rest_logs
|
||||
encoding:
|
||||
codec: 'json'
|
||||
method: 'post'
|
||||
request:
|
||||
retry_max_duration_secs: 30
|
||||
retry_initial_backoff_secs: 1
|
||||
headers:
|
||||
x-api-key: ${LOGFLARE_PUBLIC_ACCESS_TOKEN?LOGFLARE_PUBLIC_ACCESS_TOKEN is required}
|
||||
uri: 'http://analytics:4000/api/logs?source_name=postgREST.logs.prod'
|
||||
logflare_db:
|
||||
type: 'http'
|
||||
inputs:
|
||||
- db_logs
|
||||
encoding:
|
||||
codec: 'json'
|
||||
method: 'post'
|
||||
request:
|
||||
retry_max_duration_secs: 30
|
||||
retry_initial_backoff_secs: 1
|
||||
headers:
|
||||
x-api-key: ${LOGFLARE_PUBLIC_ACCESS_TOKEN?LOGFLARE_PUBLIC_ACCESS_TOKEN is required}
|
||||
# Route directly to analytics like other sinks. Retry handles the startup
|
||||
# race where analytics may not be ready yet when early DB logs arrive.
|
||||
uri: 'http://analytics:4000/api/logs?source_name=postgres.logs'
|
||||
logflare_functions:
|
||||
type: 'http'
|
||||
inputs:
|
||||
- functions_logs
|
||||
encoding:
|
||||
codec: 'json'
|
||||
method: 'post'
|
||||
request:
|
||||
retry_max_duration_secs: 30
|
||||
retry_initial_backoff_secs: 1
|
||||
headers:
|
||||
x-api-key: ${LOGFLARE_PUBLIC_ACCESS_TOKEN?LOGFLARE_PUBLIC_ACCESS_TOKEN is required}
|
||||
uri: 'http://analytics:4000/api/logs?source_name=deno-relay-logs'
|
||||
logflare_storage:
|
||||
type: 'http'
|
||||
inputs:
|
||||
- storage_logs
|
||||
encoding:
|
||||
codec: 'json'
|
||||
method: 'post'
|
||||
request:
|
||||
retry_max_duration_secs: 30
|
||||
retry_initial_backoff_secs: 1
|
||||
headers:
|
||||
x-api-key: ${LOGFLARE_PUBLIC_ACCESS_TOKEN?LOGFLARE_PUBLIC_ACCESS_TOKEN is required}
|
||||
uri: 'http://analytics:4000/api/logs?source_name=storage.logs.prod.2'
|
||||
logflare_kong:
|
||||
type: 'http'
|
||||
inputs:
|
||||
- kong_logs
|
||||
- kong_err
|
||||
encoding:
|
||||
codec: 'json'
|
||||
method: 'post'
|
||||
request:
|
||||
retry_max_duration_secs: 30
|
||||
retry_initial_backoff_secs: 1
|
||||
headers:
|
||||
x-api-key: ${LOGFLARE_PUBLIC_ACCESS_TOKEN?LOGFLARE_PUBLIC_ACCESS_TOKEN is required}
|
||||
uri: 'http://analytics:4000/api/logs?source_name=cloudflare.logs.prod'
|
||||
30
volumes/pooler/pooler.exs
Normal file
30
volumes/pooler/pooler.exs
Normal file
@@ -0,0 +1,30 @@
|
||||
{:ok, _} = Application.ensure_all_started(:supavisor)
|
||||
|
||||
{:ok, version} =
|
||||
case Supavisor.Repo.query!("select version()") do
|
||||
%{rows: [[ver]]} -> Supavisor.Helpers.parse_pg_version(ver)
|
||||
_ -> nil
|
||||
end
|
||||
|
||||
params = %{
|
||||
"external_id" => System.get_env("POOLER_TENANT_ID"),
|
||||
"db_host" => System.get_env("POSTGRES_HOST") || "db",
|
||||
"db_port" => System.get_env("POSTGRES_PORT"),
|
||||
"db_database" => System.get_env("POSTGRES_DB"),
|
||||
"require_user" => false,
|
||||
"auth_query" => "SELECT * FROM pgbouncer.get_auth($1)",
|
||||
"default_max_clients" => System.get_env("POOLER_MAX_CLIENT_CONN"),
|
||||
"default_pool_size" => System.get_env("POOLER_DEFAULT_POOL_SIZE"),
|
||||
"default_parameter_status" => %{"server_version" => version},
|
||||
"users" => [%{
|
||||
"db_user" => "pgbouncer",
|
||||
"db_password" => System.get_env("POSTGRES_PASSWORD"),
|
||||
"mode_type" => System.get_env("POOLER_POOL_MODE"),
|
||||
"pool_size" => System.get_env("POOLER_DEFAULT_POOL_SIZE"),
|
||||
"is_manager" => true
|
||||
}]
|
||||
}
|
||||
|
||||
if !Supavisor.Tenants.get_tenant_by_external_id(params["external_id"]) do
|
||||
{:ok, _} = Supavisor.Tenants.create_tenant(params)
|
||||
end
|
||||
18
volumes/proxy/caddy/Caddyfile
Normal file
18
volumes/proxy/caddy/Caddyfile
Normal file
@@ -0,0 +1,18 @@
|
||||
{$PROXY_DOMAIN} {
|
||||
@supabase_api path /auth/v1/* /rest/v1/* /graphql/v1 /realtime/v1/* /storage/v1/* /functions/v1/* /mcp /sso/*
|
||||
|
||||
handle @supabase_api {
|
||||
reverse_proxy kong:8000
|
||||
}
|
||||
|
||||
handle {
|
||||
basic_auth {
|
||||
# PROXY_AUTH_PASSWORD is overwritten with the bcrypt on startup
|
||||
{$PROXY_AUTH_USERNAME} {$PROXY_AUTH_PASSWORD}
|
||||
}
|
||||
|
||||
reverse_proxy studio:3000
|
||||
}
|
||||
|
||||
header -server
|
||||
}
|
||||
95
volumes/proxy/nginx/supabase-nginx.conf.tpl
Normal file
95
volumes/proxy/nginx/supabase-nginx.conf.tpl
Normal file
@@ -0,0 +1,95 @@
|
||||
upstream kong_upstream {
|
||||
server kong:8000;
|
||||
keepalive 2;
|
||||
}
|
||||
|
||||
server {
|
||||
listen 443 ssl;
|
||||
listen [::]:443 ssl;
|
||||
http2 on;
|
||||
|
||||
server_name ${PROXY_DOMAIN};
|
||||
server_tokens off;
|
||||
|
||||
proxy_http_version 1.1;
|
||||
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $remote_addr;
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
proxy_set_header X-Forwarded-Host $http_host;
|
||||
proxy_set_header X-Forwarded-Port $server_port;
|
||||
|
||||
ssl_certificate /etc/letsencrypt/live/${PROXY_DOMAIN}/fullchain.pem;
|
||||
ssl_certificate_key /etc/letsencrypt/live/${PROXY_DOMAIN}/privkey.pem;
|
||||
ssl_trusted_certificate /etc/letsencrypt/live/${PROXY_DOMAIN}/chain.pem;
|
||||
|
||||
ssl_dhparam /etc/letsencrypt/dhparams/dhparam.pem;
|
||||
|
||||
ssl_session_cache shared:SSL:10m;
|
||||
ssl_session_timeout 10m;
|
||||
|
||||
# Prevent 502 errors from large Supabase auth cookies
|
||||
large_client_header_buffers 4 16k;
|
||||
proxy_buffer_size 128k;
|
||||
proxy_buffers 4 256k;
|
||||
proxy_busy_buffers_size 256k;
|
||||
|
||||
location / {
|
||||
auth_basic "supabase";
|
||||
auth_basic_user_file /etc/nginx/user_conf.d/dashboard-passwd;
|
||||
|
||||
proxy_pass http://studio:3000;
|
||||
}
|
||||
|
||||
location /auth {
|
||||
proxy_pass http://kong_upstream;
|
||||
}
|
||||
|
||||
location /rest {
|
||||
proxy_pass http://kong_upstream;
|
||||
}
|
||||
|
||||
location /graphql {
|
||||
proxy_pass http://kong_upstream;
|
||||
}
|
||||
|
||||
location /realtime/v1/ {
|
||||
proxy_pass http://kong_upstream;
|
||||
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $remote_addr;
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
proxy_set_header X-Forwarded-Host $http_host;
|
||||
proxy_set_header X-Forwarded-Port $server_port;
|
||||
|
||||
proxy_set_header Upgrade $http_upgrade;
|
||||
proxy_set_header Connection "upgrade";
|
||||
|
||||
proxy_read_timeout 3600s;
|
||||
}
|
||||
|
||||
location /storage/v1/ {
|
||||
proxy_pass http://kong_upstream;
|
||||
|
||||
proxy_buffering off;
|
||||
proxy_request_buffering off;
|
||||
|
||||
chunked_transfer_encoding off;
|
||||
|
||||
client_max_body_size 0;
|
||||
}
|
||||
|
||||
location /functions {
|
||||
proxy_pass http://kong_upstream;
|
||||
}
|
||||
|
||||
location /mcp {
|
||||
proxy_pass http://kong_upstream;
|
||||
}
|
||||
|
||||
location /sso {
|
||||
proxy_pass http://kong_upstream;
|
||||
}
|
||||
}
|
||||
0
volumes/snippets/.gitkeep
Normal file
0
volumes/snippets/.gitkeep
Normal file
16
volumes/snippets/Untitled query 224.sql
Normal file
16
volumes/snippets/Untitled query 224.sql
Normal file
@@ -0,0 +1,16 @@
|
||||
CREATE OR REPLACE FUNCTION public.check_email_exists(p_email text)
|
||||
RETURNS boolean
|
||||
LANGUAGE sql
|
||||
SECURITY DEFINER
|
||||
SET search_path = auth
|
||||
AS $$
|
||||
SELECT EXISTS(
|
||||
SELECT 1
|
||||
FROM auth.users
|
||||
WHERE email = p_email
|
||||
AND deleted_at IS NULL
|
||||
);
|
||||
$$;
|
||||
|
||||
GRANT EXECUTE ON FUNCTION public.check_email_exists(text) TO anon;
|
||||
GRANT EXECUTE ON FUNCTION public.check_email_exists(text) TO authenticated;
|
||||
Reference in New Issue
Block a user