Skip to content

cloud/network-policy.mdx: page assumes boxes start private, but the create path defaults public=true (~10 passages + missing row in vs-opensource) #35

Description

@Mandalorian-Wang

Summary

Every part of cloud/network-policy.mdx is written on the premise that a new box starts private. The implementation does the opposite: a box is created with public = true, so its ports are reachable by anyone holding the preview URL until you explicitly close them.

This is not one wrong sentence — the page's whole mental model ("start private → escalate → reserve the public flag for deliberate publishing") is inverted relative to actual behaviour. Roughly ten passages depend on the wrong premise, and the cloud/vs-opensource.mdx comparison table is missing the difference entirely, which is where a self-hosted user migrating to Cloud would most likely look.

Ground truth (implementation)

Three facts from boxlite-ai/boxlite (main) that together define the actual behaviour:

1. The create path defaults public to trueapps/api/src/box/services/box.service.ts:286

box.public = createBoxDto.public ?? true

2. The entity/DB default is the opposite (false)apps/api/src/box/entities/box.entity.ts:111-112

@Column({ default: false, type: 'boolean' })
public = false

So the column default never takes effect on the normal create path; :286 overrides it. (Worth a look on the BoxLite side — the two defaults disagreeing is at least confusing, and it made the intended behaviour hard to determine from code alone.)

3. Inbound on the /v1 REST surface is expressed as network.inbound.mode, not publicapps/api/src/boxlite-rest/dto/create-box.dto.ts:84-87

// mode="enabled" means services the box exposes are publicly reachable;
// mode="disabled" means private.

Observed behaviour (matches the code)

Freshly created box, a service on 8080, no visibility call made, requests from a clean client (no cookies, no headers, no redirects followed):

GET /api/box/{id}                    → "public": true      ← created public
anonymous GET <preview-url>/         → 200 + real response body
POST /api/box/{id}/public/false      → 201
anonymous GET <preview-url>/         → 307                 ← now closed
POST /api/box/{id}/public/true       → 201
anonymous GET <preview-url>/         → 200

Two related observations while confirming this:

  • POST /v1/boxes with {"public": false} is silently ignored — the created box still reports "public": true. The /v1 create DTO models inbound as network.inbound.mode, so a public key in that request body is simply dropped. There is currently no documented way to create a closed box in one call; you create it (already reachable) and then close it.
  • The SDK cannot read visibility at allBoxInfo has no public attribute, so rt.list_info() cannot answer "which of my boxes are reachable". Only GET /api/box/{id} exposes it. (Filed separately on the BoxLite side.)

Verified against a BoxLite Cloud API endpoint; SDK built from boxlite main.

Documentation passages that are wrong

All in cloud/network-policy.mdx (line numbers approximate):

Line Current text Why it's wrong
4 description: "keep it private, share one port through a signed link, or make it public" "keep it" presumes it starts private
38 isPublic table: "false returns it to private" "returns it to" presumes private was the original state
51 "Be clear-eyed about what you just did. A public box drops the credential requirement in front of it: anyone who has a URL can reach the service…" Framed as the consequence of turning the flag on. It is in fact the description of the default state
87 Heading: "Present the token when the box is private" The default state needs no token
89 "A private box's preview URL is not open, so a request has to carry the token" Default state serves anonymous requests
103 "The query-parameter form is what makes a private preview URL openable in a browser" It is already openable
105 "A request that carries no credential the proxy accepts is redirected to sign in" Anonymous requests get 200, not a sign-in redirect
160–162 Decision table — all three rows begin "Private box…" / "Public box" Presents private as the starting point
164 "The order matters: start private, escalate to a signed URL when a human needs to see something, and reserve the public flag for a box whose contents you would publish deliberately." The most direct contradiction: the order is the reverse
251 Troubleshooting: "Fetching a preview URL returns unauthorized → The box is private, and the request carried no credential" A new box is not private, so this diagnosis misleads

And one omission, arguably the highest-impact of all:

File Problem
cloud/vs-opensource.mdx:13-27 The Cloud-vs-open-source table covers auth, exec shape, storage, snapshots, lifecycle, limits, cost — but not inbound reachability. The "Exposing a service" row (:22) only says Cloud gives you "a tunnel to a guest port, which carries a public URL". A self-hosted user carries over the mental model "nothing is reachable unless I forward a port", which does not hold on Cloud.

Also minor: :13 prerequisites say write access is needed "for the public flag", implying permission is only relevant when opening a box. The same endpoint and permission are needed to close one, which is the security-relevant direction.

Suggested changes

1. Flip the page's framing rather than patching sentences. Because ~10 passages inherit the premise, sentence-level edits will leave stale assumptions behind. Suggested new spine: "A box ships reachable — close what you do not intend to publish." Concretely:

  • Rewrite :164 as the reverse order, e.g. "A new box's ports are reachable by anyone holding its preview URL. Close anything you do not intend to publish with POST /api/box/{boxIdOrName}/public/false, and reopen it deliberately when you want it published."
  • Move the substance of :51 up into the page intro and reframe it as a description of the default state, not of the flag's effect.
  • Rewrite the :87–105 block so the token forms are presented as "how to reach a box you have closed", not "how to reach a private box".
  • In the decision table (:160–162), make the first row the default state, and label the others as deliberate departures from it.
  • Fix the troubleshooting row (:251) so the diagnosis reads "the box has been closed with public/false".
  • Adjust :38 ("returns it to private""closes the box to unauthenticated access").

2. Add an inbound-reachability row to cloud/vs-opensource.mdx. Something like:

Dimension Open source (self-hosted) Cloud
Inbound reachability Nothing is reachable from outside unless you forward a port A new box's ports are reachable by anyone holding its preview URL. Close a box with POST /api/box/{boxIdOrName}/public/false.

3. State how to create a closed box — or state that you cannot. Right now the only documented sequence is create-then-close, which leaves a window during which the box is reachable. If POST /v1/boxes gains a working way to express this (the /v1 DTO already models inbound as network.inbound.mode, and mode: "disabled" is documented as meaning private), the page should show it. Until then it's worth saying plainly that closing is a second call, so readers can decide whether that window matters to them.

4. Document how to read the current state. The page documents POST …/public/{isPublic} for setting visibility but never mentions that GET /api/box/{boxIdOrName} returns a public field. Given the default, "how do I audit which of my boxes are reachable" is a question readers will have.

Context on the default itself

To be clear about scope: I'm told the reachable-by-default behaviour is intentional on the BoxLite side — a box should be easy to build and publish — so this issue is not asking for the behaviour to change. It's asking for the docs to describe it, since visibility is a security-relevant property and the page currently teaches the opposite.

Related: #34 covers the signed-preview-URL port restriction on the same page, and includes a shorter version of this default-visibility note. Happy to have this issue supersede that section of #34 so the two don't overlap.

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't workingdocumentationImprovements or additions to documentation

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions