Skip to content

Security

What Peekaboot exposes while it is on, and how to put a lock in front of it.

On a local run anyone who can reach your application’s port can read everything under /peekaboot/** without signing in: configuration, environment values, health, logs, migrations and full request traces. Peekaboot’s own guard is off there. If anyone other than you can reach that port, secure /peekaboot/** first or turn Peekaboot off.

Who can reach Peekaboot, and when it is on

Peekaboot switches itself on or off from the way the application was launched. An explicit property always wins.

Launch peekaboot.enabled dev-toolbar, storage.enabled, error-page.enabled security.enabled
Local run: IDE, spring-boot:run, bootRun, java -cp target/classes … on a host that is not a container true true false
Deployment: java -jar, a war, a native image, anything in a container false false true
Test false false false

Every switch except peekaboot.enabled itself also needs peekaboot.enabled=true. What counts as a local run has the full detection rules.

What that means for access:

Security-related properties

Property Default Effect
peekaboot.enabled detected Serves the dashboard and its API. Off, nothing exists under /peekaboot/**.
peekaboot.dev-toolbar detected Injects the toolbar and captures headers, parameters and log messages into traces.
peekaboot.enable-unmasking false Allows ?unmask=true to return real values. See Masking opt-ins.
peekaboot.security.enabled detected Arms the HTTP Basic fallback guard.
peekaboot.security.username <artifact>-admin The guard’s username.
peekaboot.security.password unset The guard’s password. Unset, one is generated.
peekaboot.security.credentials-file security.properties in the storage directory Where the generated password’s hash is stored. A path you set is written even while storage is off.
peekaboot.storage.enabled detected Whether Peekaboot writes any file.
peekaboot.storage.dir ${user.home}/.peekaboot/<groupId>.<artifactId> Where those files go.
peekaboot.error-page.enabled detected Serves the error page with exception and stack trace.
peekaboot.error-page.override false Serves it even where the application has an error page of its own.

Types and the remaining details are in Configuration.

What the dashboard and API show

This is the complete list. Anyone who can reach /peekaboot/** can read all of it.

Data Where Masked
Every resolved property source, key and value Environment tab By key and by value shape
Every @ConfigurationProperties value Config tab By key and by value shape
Health status and per-component detail, custom indicators included Overview By key and by value shape
Build and Git metadata (info.build) Overview By key and by value shape
Datasource host, port, database name, user, product and driver version Overview Connection parameters only. User, host and database name are shown as is.
Process identity: OS user, uid, gid, pid, parent-process chain with command names Overview No
Machine: CPU count and model, physical memory, max heap, container runtime, every non-local IP address and its hostname Overview, runtime.machine in the API No
Logger levels, configured and effective Loggers tab Nothing to mask. The API is GET-only, so levels cannot be changed.
Flyway migrations: version, description, script, type, duration, install time, status Flyway tab No
@Scheduled tasks: schedule, last and next run, last failure’s exception type and message Scheduled Tasks tab Failure text by value shape
Start and stop history with version, branch, commit and build time per run, unclean shutdowns Lifecycle tab, /peekaboot/api/lifecycle/** No
Micrometer meters: names, tags, measurements Meters tab Tag values by key and by value shape
Up to 30 days of CPU, memory, thread, HTTP, connection-pool and log-event history, and the list of collected meters Insights tab, /peekaboot/api/insights/** No
Request traces: span tree, span names, timings, tags, error messages, SQL text and bind parameters Traces tab, toolbar Tags, error messages, SQL and bind parameters by value shape only
With the toolbar on: request and response headers, query and form parameters, resolved controller Traces tab, toolbar Headers and parameters by key and by value shape
With the toolbar on: the message of every log event emitted inside a trace Logs tab of a trace No

SQL text is shown as your JDBC instrumentation records it, literal values included where the statement carries them. Bind parameters are shown too, recorded on the query’s span. An application that also exports its traces, over OTLP say, sends them along unmasked. Request and response bodies are never captured. A log event emitted outside a trace is dropped.

With the toolbar on, every response carries Server-Timing: trace;desc="00-<traceId>-<spanId>-<flags>", JSON API calls included. Anyone who reads that header and can reach /peekaboot/** can open that request’s trace at GET /peekaboot/api/traces/{traceId}/insights. Responses under /peekaboot/**, your management base path, /static/**, /webjars/** and /error/** get no header.

Every /peekaboot/api/** response carries Cache-Control: no-store and X-Content-Type-Options: nosniff, so a proxy or the browser’s cache does not keep it.

What the error page shows

The error page shows every detail Spring Boot’s ErrorAttributes has: the status, the request line, the exception class, its message and the full stack trace. None of it is masked, and your spring.web.error.include-* settings do not limit it. Requests that ask for JSON get Spring Boot’s normal error response instead.

It is on for a local run and off everywhere else. By default it replaces only Spring Boot’s whitelabel page, so an application with its own error page keeps it unless you set peekaboot.error-page.override=true. peekaboot.error-page.enabled=false turns it off. See Configuration, peekaboot.error-page.

What stays in memory and what goes to disk

Request traces, captured headers and parameters, log messages, environment values and config values stay in memory for the life of the process. They are never written to disk.

Peekaboot writes files only while peekaboot.storage.enabled is true, which is the default for a local run only. The one exception is security.properties with an explicit peekaboot.security.credentials-file.

File Written when Contents
insights.snapshot Storage on The charts’ aggregated numbers, keyed by series id. No request data, property values or traces.
lifecycle.jsonl Storage on One line per start or stop: timestamp, pid, and version, time, branch, commit.id, commit.id.full, commit.id.abbrev, build.version, build.time.
security.properties The guard generated a password, and storage is on or credentials-file is set A PBKDF2 hash of the generated password. Never the password itself.

lifecycle.jsonl drops every other build-info and git-info entry, including the git remote URL, the building user’s name and mail address, and anything else your build wrote into build-info.properties.

The files live under ${user.home}/.peekaboot/<groupId>.<artifactId>/ unless peekaboot.storage.dir says otherwise. On a POSIX file system the directory is created rwx------ and the files rw-------. See Configuration, peekaboot.storage.

Peekaboot leaves /actuator alone

Peekaboot reads Actuator’s data in-process. It adds nothing to management.endpoints.web.exposure, and which endpoints /actuator/** serves is the same with or without it. Don’t use management.endpoints.web.exposure to protect Peekaboot: it has no effect on /peekaboot/**.

Peekaboot does set management.info.env, .java, .os and .process.enabled to true while it is on. If you expose /actuator/info, it carries that extra content. See Configuration, Spring Boot defaults Peekaboot changes.

With Peekaboot off, nothing is served under /peekaboot/**, the UI assets included.

Actuator’s show-values does not apply

management.endpoint.env.show-values and management.endpoint.configprops.show-values have no effect on the dashboard. Setting them to never does not blank the Environment and Config tabs. Peekaboot’s own masking decides what those tabs hide. Peekaboot never sets either property, so your own /actuator/env and /actuator/configprops behave as you configured them.

Masking

Masking is on by default and has nothing to configure. It replaces a value with ****** in:

Your own SanitizingFunction beans also run on environment and config values, so a value they mask reaches the dashboard as ******.

What gets masked, and how

Two rule sets run together.

By key name. A key is sensitive when one of these words appears in it as a whole token, separated by any character other than a letter or digit, or by a camelCase boundary. Case does not matter.

password              passwords             passwd
passwds               pwd                   passphrase
passphrases           secret                secrets
client-secret         client-secrets        token
tokens                access-token          refresh-token
id-token              auth-token            bearer
credential            credentials           api-key
api-keys              apikey                apikeys
access-key            access-keys           private-key
private-keys          secret-key            secret-keys
signing-key           signing-keys          encryption-key
encryption-keys       authorization         auth
session-id            salt                  signature
sig                   certificate-password  certificate-private-key

By value shape. These patterns mask a credential inside any value, whatever its key. Only the matched part is masked, so a JDBC URL keeps its host and database name.

Shape Example
JWT eyJhbGciOi….eyJzdWIiOi….…
PEM private key, header to footer -----BEGIN PRIVATE KEY----- … -----END PRIVATE KEY-----
AWS access key AKIA…, ASIA…
GitHub token ghp_…, gho_…, ghu_…, ghs_…, ghr_…, github_pat_…
GCP API key AIza…
Slack token xoxb-…, xoxp-…
Stripe live key sk_live_…, rk_live_…
OpenAI key sk-proj-…, legacy sk-…
Anthropic key sk-ant-…
Credentials in a URL postgres://user:secret@host, redis://:secret@host
Oracle thin URL jdbc:oracle:thin:user/secret@host
URL or connection-string parameter with a sensitive name ?password=…, &token=…, ;pwd=…
Command-line option with a sensitive name -Dspring.datasource.password=…, --api-key=… in JAVA_TOOL_OPTIONS

A parameter or option is judged by its name with the key-name rules above. It needs its marker: a bare password=hunter2 without a leading ?, &, ;, -D or -- is not masked. A PEM block without a footer masks to the end of the value.

The key names and value shapes above are the complete set.

Masking is not exhaustive. A credential with no recognisable shape under a key that is not listed is shown as is. INSERT INTO users (password) VALUES ('hunter2') is not masked, because SQL masking goes by value shape only and does not know column names. Assume any captured trace can contain plaintext SQL and plaintext request data.

What is left unmasked entirely

Two opt-ins before a real value is shown

Masked values are revealed only when both are true:

  1. peekaboot.enable-unmasking=true. The default is false.
  2. The request is GET /peekaboot/api/actuator/all/insights?unmask=true. No other endpoint accepts unmask.

The parameter alone does nothing. With unmasking enabled, the Environment and Config tabs show a “Show secrets” toggle. It is absent otherwise. Its state is not kept: a reload or a new tab starts masked. See The dashboard.

The Config tab with the spring.datasource group expanded, its password value rendered as ****** alongside real values for its other properties, with a Show secrets toggle above the group list.
Masked, the default.
The same spring.datasource group after clicking Show secrets. Its password value now reads sample_app_db_pwd instead of ******, and everything else on the tab is unchanged.
Revealed, with enable-unmasking on and Show secrets clicked.

Securing the dashboard with Spring Security

Put a SecurityFilterChain in front of /peekaboot/** that requires a specific role:

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.Ordered;
import org.springframework.core.annotation.Order;
import org.springframework.security.config.Customizer;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.provisioning.InMemoryUserDetailsManager;
import org.springframework.security.web.SecurityFilterChain;

@Configuration
public class PeekabootSecurityConfig {

    @Bean
    @Order(Ordered.HIGHEST_PRECEDENCE)
    public SecurityFilterChain peekabootSecurityFilterChain(HttpSecurity http) throws Exception {
        return http.securityMatcher("/peekaboot/**")
                .authorizeHttpRequests(auth -> auth.anyRequest().hasRole("ADMIN"))
                .httpBasic(Customizer.withDefaults())
                .build();
    }

    // Stands in for your application's own chain. No @Order, so it is evaluated last.
    @Bean
    public SecurityFilterChain applicationSecurityFilterChain(HttpSecurity http) throws Exception {
        return http.authorizeHttpRequests(auth -> auth.anyRequest().permitAll()).build();
    }

    // Stands in for wherever your users and roles already come from.
    @Bean
    public UserDetailsService userDetailsService() {
        return new InMemoryUserDetailsManager(
                User.withUsername("admin")
                        .password("{noop}admin-password")
                        .roles("ADMIN")
                        .build(),
                User.withUsername("user")
                        .password("{noop}user-password")
                        .roles("USER")
                        .build());
    }
}

Two rules make this work:

  1. The Peekaboot chain goes first. Give it securityMatcher("/peekaboot/**") and @Order(Ordered.HIGHEST_PRECEDENCE). Leave your application’s chain without @Order. Don’t add a /peekaboot/** rule to your general chain instead, and don’t give that chain a lower order. Either way it matches /peekaboot/** first and the role check never runs.
  2. Don’t paste applicationSecurityFilterChain. It permits every request. If your application already has a chain, keep it and add only the first bean. If it has none, note that declaring any SecurityFilterChain switches off Spring Boot’s default chain: every request outside /peekaboot/** then passes unsecured unless you add a chain for it.

Replace httpBasic with whatever your application uses: form login, OAuth2, a gateway header. Use .authenticated() instead of .hasRole(...) only if every signed-in user may read everything listed under What the dashboard and API show.

The dev toolbar asks the reader to sign in

The dev toolbar is injected into every HTML page, whoever is reading it. Its script and data load from /peekaboot/**, so your chain decides who sees a filled toolbar. A reader your chain refuses sees an empty bar with this link:

Peekaboot toolbar could not start — sign in, or check that its script is allowed to load

The link opens /peekaboot/. The browser gets the 401 challenge and asks for credentials. After signing in, the toolbar fills on the next page load.

If your application sends a Content-Security-Policy:

If nothing else secures it

On a deployment launch with peekaboot.enabled=true, Peekaboot arms an HTTP Basic guard (realm Peekaboot) on /peekaboot/**. It challenges every request that does not already carry an authenticated, non-anonymous Spring Security principal. Without Spring Security on the class path, it challenges every request.

The credentials:

On a deployment, storage is off by default, so the generated password is not saved and changes on every restart. Keep it stable with one of these:

  • peekaboot.security.password, set from a secret
  • peekaboot.storage.enabled=true on a persistent home directory
  • peekaboot.security.credentials-file pointing at a persistent path, for example a mounted volume in a container

Rules to know:

Use the SecurityFilterChain above instead of relying on this guard. It has one flat credential and no roles.

Running it on a shared or deployed server

Read Do I want this in production? first. Then:

  1. Set peekaboot.enabled=true. The toolbar, storage and error page stay off unless you set them too.
  2. Put the SecurityFilterChain in front of /peekaboot/** before you deploy. Gate on a role, because whoever passes it reads everything under What the dashboard and API show.
  3. Restrict network reach as well: an internal-only ingress, a VPN, or a proxy that forwards /peekaboot/** only from trusted sources.
  4. Leave peekaboot.enable-unmasking=false.
  5. If you rely on the fallback guard, keep its password stable (see above).

Production checklist