Cookbook: Securing Actuator Endpoints¶
Goal: keep /actuator/* reachable for orchestrators and operators but not for the public internet. Health payloads leak topology (component names, error messages, versions) — treat them as internal.
Option 1: Do not expose them (preferred)¶
If the app sits behind an ingress/reverse proxy, block the prefix at the edge and let probes reach the pod directly (kubelet probes bypass the ingress):
Nothing to code — the endpoints stay fully open inside the cluster, which is what kubelet needs.
Option 2: Guard them in-app¶
When there is no trusted edge, add a FastApiConfigurer (a pico-fastapi protocol, collected automatically like indicators) that installs a small middleware:
import secrets
from pico_ioc import component
from starlette.responses import JSONResponse
@component
class ActuatorGuard: # satisfies FastApiConfigurer
def __init__(self, settings: SecuritySettings):
self.token = settings.actuator_token
def configure(self, app):
@app.middleware("http")
async def guard(request, call_next):
if request.url.path.startswith("/actuator") and request.url.path != "/actuator/health/live":
supplied = request.headers.get("x-actuator-token", "")
if not secrets.compare_digest(supplied, self.token):
return JSONResponse({"detail": "forbidden"}, status_code=403)
return await call_next(request)
Probes then send the header:
readinessProbe:
httpGet:
path: /actuator/health/ready
port: 8000
httpHeaders:
- name: x-actuator-token
value: "<from a Secret>"
Notes¶
- Leave
/health/liveopen. It reveals nothing ({"status":"UP"}) and keeping it token-free means a rotated secret can never cause a restart storm. secrets.compare_digest, not==— constant-time comparison.- Least detail off-cluster: if the payload must cross a boundary, set
actuator.show_components: falseand keep the detail for internal calls.