Skip to content

fix(docker): resolve the healthcheck port instead of passing a literal - #3687

Open
qianeric-backup wants to merge 1 commit into
beefproject:masterfrom
qianeric-backup:fix/docker-healthcheck-port
Open

qianeric-backup wants to merge 1 commit into
beefproject:masterfrom
qianeric-backup:fix/docker-healthcheck-port

Conversation

@qianeric-backup

Copy link
Copy Markdown

Category

Bug

Feature/Issue Description

Q: Please give a brief summary of your feature/fix
A:

The image healthcheck can never report healthy. It was declared in JSON exec form:

HEALTHCHECK --interval=30s --timeout=30s --start-period=5s --retries=3 CMD [ "curl", "-fS", "localhost:$UI_PORT" ]

Exec-form healthchecks do not run through a shell, and Docker stores each argv element verbatim rather than interpolating variables inside it. The built image therefore invokes curl with the literal string localhost:$UI_PORT, which curl rejects before it opens a connection:

$ curl -fS 'localhost:$UI_PORT'
curl: (3) URL rejected: Port number was not a decimal number between 0 and 65535

Q: Give a technical rundown of what you have changed (if applicable)
A:

Two changes, because either one alone leaves the check broken:

  1. UI_PORT and the sibling ports are also exported as ENV values. A build argument is not visible at run time, so a shell-form healthcheck would otherwise expand ${UI_PORT} to an empty string and request http://localhost:.
  2. The healthcheck moves to shell form so expansion happens when the check runs, with an explicit || exit 1 so curl's status is still what Docker records.
 ARG UI_PORT=3000
 ARG PROXY_PORT=6789
 ARG WEBSOCKET_PORT=61985
 ARG WEBSOCKET_SECURE_PORT=61986
 
+# Promote the port arguments to environment variables. Build arguments are not
+# visible once the image runs, and exec-form healthchecks never expand
+# variables, so the healthcheck below needs a run-time resolvable value.
+ENV UI_PORT=${UI_PORT} \
+    PROXY_PORT=${PROXY_PORT} \
+    WEBSOCKET_PORT=${WEBSOCKET_PORT} \
+    WEBSOCKET_SECURE_PORT=${WEBSOCKET_SECURE_PORT}
+
-HEALTHCHECK --interval=30s --timeout=30s --start-period=5s --retries=3 CMD [ "curl", "-fS", "localhost:$UI_PORT" ]
+# The shell form is required here: Docker stores exec-form argv verbatim and
+# does not interpolate variables inside an argument, so the healthcheck would
+# request a port literally named "$UI_PORT" and always report unhealthy.
+HEALTHCHECK --interval=30s --timeout=30s --start-period=5s --retries=3 \
+  CMD curl -fS "http://localhost:${UI_PORT}" || exit 1

curl is already present in the final stage, so no package changes are needed.

Test Cases

Q: Describe your test cases, what you have covered and if there are any use cases that still need addressing.
A:

Docker is not available in my environment, so I could not build the image and inspect docker image inspect --format '{{json .Config.Healthcheck.Test}}'. Instead I verified the two command forms directly against a listener on port 3000 (python3 -m http.server 3000):

command result exit
old exec-form argv curl: (3) URL rejected: Port number was not a decimal number between 0 and 65535 3
new shell form with UI_PORT=3000 returns the response body 0

This covers the failure mode reported in the issue. It does not cover a build with --build-arg UI_PORT=<other>, and that is deliberate: see the open question below.

Open question for the maintainers

UI_PORT currently has no effect on the application. BeEF reads its listen address from config.yaml (http.port, default 3000), which the image ships unchanged, so --build-arg UI_PORT=4321 only changes EXPOSE metadata and this healthcheck, not what BeEF actually binds. With this PR a non-default UI_PORT will now point the healthcheck at a port nothing listens on, where before the check failed for a different reason.

Fixing that properly means either wiring the build argument into the generated config or documenting UI_PORT as metadata only. Both are larger decisions than a healthcheck fix, so this PR keeps the existing relationship and only makes the check resolve its port. If you would rather have the healthcheck simply hard-code the application default until the configuration story is settled, say so and I will adjust.

Fixes #3626

Wiki Page

Not applicable; the Docker workflow is unchanged apart from the healthcheck reporting correctly.

The healthcheck was declared in JSON exec form:

    CMD [ "curl", "-fS", "localhost:$UI_PORT" ]

Exec-form healthchecks do not run through a shell, and Docker does not
interpolate variables inside an argv element, so the image stored the
literal string "localhost:$UI_PORT" and curl rejected it before opening a
connection:

    curl: (3) URL rejected: Port number was not a decimal number between 0 and 65535

The container therefore never reported healthy, even with BeEF listening
on the default port.

Two changes are needed, because either one alone is insufficient:

- UI_PORT and the sibling ports are now also exported as environment
  variables. A build argument is not visible at run time, so a shell-form
  healthcheck would otherwise expand ${UI_PORT} to an empty string.
- The healthcheck uses the shell form so that expansion happens when the
  check runs, plus an explicit `|| exit 1` to keep curl's status as the
  healthcheck status.

Verified with a listener on port 3000: the old command exits 3, the new one
returns the response and exits 0.

Note this keeps the existing relationship between the build arguments and
the application: BeEF reads its listen address from config.yaml, so a
non-default UI_PORT still only affects the image metadata and this check.
Aligning the two would be a separate change.

Refs beefproject#3626

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

fix(docker): healthcheck passes literal $UI_PORT to curl

1 participant