Every command in this post still runs word for word, the release you are on is the last one where you must do what it says to make them run, and the nodes/proxy it tells you to grant is the right subresource for the API server and the wrong one for the kubelet standing behind it¶
Post — Kubernetes 1.27: Query Node Logs Using The Kubelet API, 2023-04-21.
3,881 bytes over 85 lines, by Aravindh Puthiyaparambil (Red Hat), seventy-second of the year's
seventy-eight posts by size and so the seventh smallest of them. It is a SIG Windows feature
announcement written for an alpha. It is also the only post in the archive that dates its own
amendment: grepping blog/_posts for the phrase added to the article returns exactly one line,
:49, recording that the warning above it arrived in early 2026 — three years after publication,
and one release before the feature went stable.
As written
The problem is stated in eight lines at :13-21. A cluster administrator debugging a service on a
node has to get onto the node, by SSH on Linux or RDP on Windows, to read that service's logs. The
post names Windows as the case that hurts most: a node reaches Ready and containers still do not
start, because of a CNI misconfiguration that nothing in the Pod status will tell you about.
The mechanism is four sentences at :25-34. The kubelet already serves a /var/log/ viewer through
the node proxy endpoint, and Node log query supplements that endpoint with a shim: on Linux it
shells out to journalctl, on Windows to the Get-WinEvent cmdlet, and it reuses the filters those
two commands already have. It then describes a heuristic for callers who do not know whether a
service logs to the system logger or to a file — check the native logger first, and if that is not
available try /var/log/<servicename>, then /var/log/<servicename>.log, then
/var/log/<servicename>/<servicename>.log.
The assumptions and the permission are :36-40: journald and an installed journalctl on Linux,
the application log provider on Windows, and fetching node logs is only available if you are
authorized to do so (in RBAC, that's get and create access to nodes/proxy). A warning
block follows at :42-47 saying that granting nodes/proxy — even just get — also authorizes
executing commands in any container on the node, and linking the kubelet authentication and
authorization reference at its #get-nodes-proxy-warning anchor. Then :49, in italics and
parentheses: this warning message was added to the article in early 2026.
The recipe is :53-58. Enable the NodeLogQuery feature gate for that node, and set both kubelet
configuration options enableSystemLogHandler and enableSystemLogQuery to true. Three commands
follow. :61 fetches a named service's logs, :67 adds &pattern=error to filter them, and :72
fetches a file from /var/log/ by putting a leading slash on the query value. All three are
kubectl get --raw against /api/v1/nodes/<node>/proxy/logs/. The post lists no other query
options; :75-77 points at the documentation for those. :79-85 asks for feedback on the
#sig-windows Slack channel and the SIG Windows mailing list.
As it runs now
Everything the post tells you to type still works, unchanged. That is the first thing, and it is
rarer in this archive than it sounds: the three commands at :61, :67 and :72 are byte-for-byte
the three commands the pinned concept page prints at system-logs.md:276, :306 and :283, node
name and all. The post and the documentation for the feature have not diverged in three years and
five months, because the documentation was written from the post.
The second thing is that the opt-in the post describes is still required on the cluster this
exercise runs on, and will not be one release later. NodeLogQuery is beta and off by default
through v1.35, and stable and on from v1.36. The lab runs v1.35. That makes this the last release in
which a reader has to do what :53-58 says in order for :61 to work — which is a strange and
useful place to stand, because the same reader can watch the instruction be necessary and read the
page that says it has stopped being necessary, in the same session.
The third thing is the six query options. The post names one, pattern, and defers the rest to the
documentation. system-logs.md:293-300 is that documentation: a six-row table giving boot,
pattern, query, sinceTime, untilTime and tailLines, one line of description each. query
is marked (required) there and nowhere in the post. The table is the complete reference for every
one of them — there is no expanded prose anywhere under content/en — so, for instance, nothing in
the pinned tree says whether pattern matches case sensitively. The only way to find out is to run
it.
The fourth thing is not a behaviour. Three times over, the pinned tree disagrees with itself about this feature, and two of the three can be settled with a command.
The first disagreement is the default of enableSystemLogHandler.
docs/concepts/cluster-administration/system-logs.md:255-256 says it defaults to false and is
recommended to be left disabled unless actively debugging. The generated API reference,
docs/reference/config-api/kubelet-config.v1beta1.md:1587-1588, says Default: true. They cannot
both be right, and the sample kubelet configuration at
docs/tasks/administer-cluster/kubelet-config-file.md:279-280 writes the key explicitly as true
without settling which value it would have had if omitted. A node's own running configuration
settles it, and that is step 1.
The second disagreement is what the recipe becomes at v1.36. system-logs.md:246-248 says the two
kubelet options must both be set to true. Two lines later, :250-253 says the gate is now locked
to true, leaving enableSystemLogHandler as the only option required to enable or disable the Log
Query feature — which reads as enableSystemLogQuery no longer being needed. The v1.36 release
announcement, blog/_posts/2026/kubernetes-v1-36-release/index.md:227, says the opposite: In
addition, the enableSystemLogQuery kubelet configuration option must also be enabled. Three
statements, two of them four lines apart on one page. A v1.35 cluster cannot settle this one, and
this exercise does not pretend to; what it can do is show that at v1.35 the gate alone is not
enough, which is the only half of the question the lab ceiling reaches.
The third disagreement is about which subresource guards the log endpoint, and it is the one the
post is entangled in. kubelet-authn-authz.md:66-75 maps kubelet API paths to resources and
subresources, and the row at :72 reads /logs/\* → nodes → log. The fine-grained table added
under the KubeletFineGrainedAuthz gate repeats it at :112, and unlike /pods, /healthz and
/configz — which list pods, proxy, healthz, proxy and configz, proxy and so fall back — the
/logs/\* row names log alone. The post says nodes/proxy, and links that very page. Both are
true, of different hops: your request goes to the API server as /api/v1/nodes/<node>/proxy/logs/,
where the subresource is proxy, and the API server then makes a second request to the kubelet as
/logs/?query=..., where the subresource is log and the identity is not yours. The post collapses
two authorizations into one sentence. Steps 8 and 9 take them apart.
One more, and it is the reason the second hop is invisible on a kubeadm cluster.
docs/setup/best-practices/certificates.md:119 puts kube-apiserver-kubelet-client in the group
system:masters. The note immediately below it, :122-125, says a less privileged group can be
used instead and that kubeadm uses the kubeadm:cluster-admins group for that purpose — present
tense, about the same certificate the table above just described differently. A command settles this
one too: read the certificate on the node.
The diff, and why
Nothing broke, and saying only that would waste the post. The three commands run. The recipe
works. The heuristic described at :29-34 is in the concept page at system-logs.md:286-289, close
enough to the post's wording that the lineage is obvious. On the axis this archive usually measures
— does the reader's cluster still answer to what the post typed — this is as clean a still right
as 2023 has.
The gate half of the recipe was retired by being agreed with, one release after this cluster's.
:53-58 tells you to enable a feature gate. From v1.36 there is no gate to enable:
system-logs.md:250-253 records that it is locked to true, which is the same sentence pattern the
project uses for every graduation, and the instruction stops being something you do and becomes
something that already happened. The reader's problem in that world is not an error message —
correct instructions that became unnecessary produce no error at all. They produce a reader who
edits a kubelet config file, restarts a kubelet, and cannot tell whether the edit did anything. This
exercise is the last chance to watch the edit matter.
The Windows half of the mechanism was never absorbed. Get-WinEvent occurs in exactly one file
under content/en, and that file is this post. The asymmetry within the post's own sentence is the
evidence. Its Linux half names journalctl, and journalctl occurs in six files under docs, one
of which — logging.md:183-184 — tells you outright to read the systemd journal with journalctl -u
kubelet. Its Windows half names Get-WinEvent, and Get-WinEvent occurs in no file under docs
at all. The nearest the concept page comes is the application log provider at
system-logs.md:267-268, which is the category and not the command. So for the question what does
the kubelet actually invoke on a Windows node, the pinned tree's only answer is a blog post from
2023 that no page under content/en links to. Grepping the whole checkout for
node-log-query-alpha returns the post and nothing else. Naming still right alone here would tell
a reader the post holds; it would not tell them that for one of the two operating systems the post
is the only thing that holds.
The gate itself is short and its stages are unremarkable, which is the point of transcribing it: the release the beta stage closes on is what makes v1.35 the interesting seat.
NodeLogQuery
| stage | default | locked | releases |
|---|---|---|---|
| alpha | false |
— | v1.27 – v1.29 |
| beta | false |
— | v1.30 – v1.35 |
| stable | true |
— | v1.36 – |
The file writes no locked key on any stage, and no removed key at all — it is one of the 257 of
487 gate files at the pin that are still listed. That absence is worth holding next to
system-logs.md:251, which says in prose that the gate is now locked to true: the gate file,
which is where this repository reads locking from, does not say so. Nine releases separate the alpha
from the stable, the beta stage runs six of them, and the stable stage is unbounded — open at v1.36
and still open at v1.37, the newest release the pin knows.
Topology — solo, fresh. One node is enough and one
node is required: every step edits that node's kubelet configuration and restarts its kubelet, and
the thing under test is what a single kubelet will and will not serve. Bring the guest up with the
five provision steps, install Kubernetes with the node
baseline procedure, then ssh
zain@10.10.10.180. Steps 1 to 9 need the cluster; step 10 needs only the pinned tree.
Do
- Read the node's running configuration before changing anything. The two options at the post's
:53-58are the subject; the third print is the gate. Note which of the two disagreeing pages the first value agrees with.
NODE=$(kubectl get nodes -o jsonpath='{.items[0].metadata.name}')
kubectl version -o json | python3 -c 'import sys,json; print("server:", json.load(sys.stdin)["serverVersion"]["gitVersion"])'
kubectl get --raw "/api/v1/nodes/$NODE/proxy/configz" \
| python3 -c 'import sys,json; c=json.load(sys.stdin)["kubeletconfig"]; print("enableSystemLogHandler:", c.get("enableSystemLogHandler")); print("enableSystemLogQuery:", c.get("enableSystemLogQuery")); print("featureGates:", c.get("featureGates", {}))'
sudo grep -n 'enableSystemLog\|featureGates' /var/lib/kubelet/config.yaml || echo "neither key is in the file"
- Run the post's first command unchanged, and then the endpoint the post says this feature supplements. One of the two works on a cluster that has done nothing.
kubectl get --raw "/api/v1/nodes/$NODE/proxy/logs/?query=kubelet" | head -5 || echo "refused"
kubectl get --raw "/api/v1/nodes/$NODE/proxy/logs/" | head -20
Record the exact refusal text from the first command. It is the message a reader who skipped
:53-58 will see, and it is not printed anywhere in the post or in the concept page.
- Enable the gate and nothing else. This is the half of the recipe that
system-logs.md:250-253says is the only half that matters from v1.36.
sudo cp /var/lib/kubelet/config.yaml /var/lib/kubelet/config.yaml.bak
printf 'featureGates:\n NodeLogQuery: true\n' | sudo tee -a /var/lib/kubelet/config.yaml
sudo systemctl restart kubelet; sleep 10
systemctl is-active kubelet
kubectl get --raw "/api/v1/nodes/$NODE/proxy/configz" \
| python3 -c 'import sys,json; print(json.load(sys.stdin)["kubeletconfig"].get("featureGates", {}))'
kubectl get --raw "/api/v1/nodes/$NODE/proxy/logs/?query=kubelet" | head -3 || echo "still refused"
- Add the second option. The recipe from the post's
:53-58is now complete, and this is the line where the post starts working.
printf 'enableSystemLogQuery: true\n' | sudo tee -a /var/lib/kubelet/config.yaml
sudo systemctl restart kubelet; sleep 10
systemctl is-active kubelet
kubectl get --raw "/api/v1/nodes/$NODE/proxy/logs/?query=kubelet" | head -5
kubectl get --raw "/api/v1/nodes/$NODE/proxy/logs/?query=kubelet" | wc -l
- Filter on the node, then run the same filter yourself, and find out what the one-line description
of
patternatsystem-logs.md:296does not say.
kubectl get --raw "/api/v1/nodes/$NODE/proxy/logs/?query=kubelet" > /tmp/kubelet-all.log
wc -l /tmp/kubelet-all.log
grep -c 'error' /tmp/kubelet-all.log || true
grep -c -i 'error' /tmp/kubelet-all.log || true
kubectl get --raw "/api/v1/nodes/$NODE/proxy/logs/?query=kubelet&pattern=error" | wc -l
kubectl get --raw "/api/v1/nodes/$NODE/proxy/logs/?query=kubelet&pattern=Error" | wc -l
- Use the four options the post never mentions. They are the rest of the table at
system-logs.md:293-300, and the post's:75-77is the sentence that sends you there.
kubectl get --raw "/api/v1/nodes/$NODE/proxy/logs/?query=kubelet&tailLines=5"
kubectl get --raw "/api/v1/nodes/$NODE/proxy/logs/?query=kubelet&boot=0" | wc -l
kubectl get --raw "/api/v1/nodes/$NODE/proxy/logs/?query=kubelet&boot=-1" | head -3 || echo "no previous boot"
SINCE=$(date -u -d '-10 min' +%Y-%m-%dT%H:%M:%SZ)
kubectl get --raw "/api/v1/nodes/$NODE/proxy/logs/?query=kubelet&sinceTime=$SINCE" | wc -l
kubectl get --raw "/api/v1/nodes/$NODE/proxy/logs/?query=kubelet" | wc -l
- Walk the heuristic at
:29-34with a service that is not a service. Nothing on this node logs to a file under a directory named after itself, so make one, and then ask for it both ways — by name, which is the heuristic, and by path, which is:72.
sudo mkdir -p /var/log/madeup
printf 'line one\nline two error\nline three\n' | sudo tee /var/log/madeup/madeup.log > /dev/null
kubectl get --raw "/api/v1/nodes/$NODE/proxy/logs/?query=madeup"
kubectl get --raw "/api/v1/nodes/$NODE/proxy/logs/?query=/madeup/madeup.log"
kubectl get --raw "/api/v1/nodes/$NODE/proxy/logs/?query=containerd" | head -3
kubectl get --raw "/api/v1/nodes/$NODE/proxy/logs/?query=nosuchservice" || echo "nothing by that name"
- Ask which subresource the API server checks for this path. The post names one at
:39-40; the page it links names another. Grant each in turn to an identity that has nothing else. The exercise on the dashboard the project marked dead already establishes thatgetonnodes/proxyis not read-only and quotes the warning that says so; what is open here is which of the two subresources this path consults, and whether thecreatehalf of the post's sentence is ever used.
kubectl create sa logreader
kubectl create clusterrole node-proxy-get --verb=get --resource=nodes/proxy
kubectl create clusterrole node-log-get --verb=get --resource=nodes/log
kubectl create clusterrolebinding logreader-bind --clusterrole=node-proxy-get --serviceaccount=default:logreader
SA=system:serviceaccount:default:logreader
kubectl auth can-i get nodes/proxy --as=$SA; kubectl auth can-i get nodes/log --as=$SA
kubectl get --raw "/api/v1/nodes/$NODE/proxy/logs/?query=kubelet" --as=$SA | head -3
kubectl delete clusterrolebinding logreader-bind
kubectl create clusterrolebinding logreader-bind --clusterrole=node-log-get --serviceaccount=default:logreader
kubectl auth can-i get nodes/proxy --as=$SA; kubectl auth can-i get nodes/log --as=$SA
kubectl get --raw "/api/v1/nodes/$NODE/proxy/logs/?query=kubelet" --as=$SA | head -3 || echo "forbidden"
- Ask which identity crosses the second hop, and read the certificate that makes the answer
uninteresting on a kubeadm cluster. The last two prints settle
certificates.md:119against the note at:122-125.
sudo grep -n -A3 'authorization:' /var/lib/kubelet/config.yaml
grep -n 'kubelet-client-certificate\|kubelet-client-key' /etc/kubernetes/manifests/kube-apiserver.yaml
kubectl get clusterrole system:kubelet-api-admin -o jsonpath='{range .rules[*]}{.resources}{"\n"}{end}'
kubectl get clusterrolebinding -o json \
| python3 -c 'import sys,json; [print(b["metadata"]["name"], "->", [s.get("name") for s in b.get("subjects") or []]) for b in json.load(sys.stdin)["items"] if b["roleRef"]["name"]=="system:kubelet-api-admin"]'
sudo openssl x509 -in /etc/kubernetes/pki/apiserver-kubelet-client.crt -noout -subject -issuer
-
Offline, in the pinned tree. Eight reads, in the order the argument was built.
cd /path/to/kubernetes/website/content/en sed -n '244,256p' docs/concepts/cluster-administration/system-logs.md sed -n '1583,1599p' docs/reference/config-api/kubelet-config.v1beta1.md sed -n '68,75p;108,117p' docs/reference/access-authn-authz/kubelet-authn-authz.md sed -n '113,126p' docs/setup/best-practices/certificates.md sed -n '227p' blog/_posts/2026/kubernetes-v1-36-release/index.md grep -rl 'Get-WinEvent' --include='*.md' . grep -rl 'node-log-query-alpha' --include='*.md' . cat docs/reference/command-line-tools-reference/feature-gates/NodeLogQuery.md
Expect
Step 1 prints a v1.35 server. featureGates is empty — kubeadm sets none — and
/var/lib/kubelet/config.yaml has neither of the two keys the post names, so both values you read
come from the API type's defaults rather than from anything on this node. Expect
enableSystemLogHandler: true and enableSystemLogQuery: false, which makes
kubelet-config.v1beta1.md:1587-1588 right and system-logs.md:255-256 wrong about that default.
If your node prints false instead, that is the publishable result and the disagreement resolves
the other way; either way the running configuration is the tiebreak, because it is the only one of
the three sources that is not prose.
Step 2 is the split the post's :25-26 describes and then never demonstrates. The bare
/proxy/logs/ request succeeds and returns an HTML directory index of /var/log on the node — that
is the pre-existing viewer, enabled by enableSystemLogHandler, and it has nothing to do with this
feature. The ?query=kubelet request fails. Record its wording; the useful part is that it comes
from the kubelet, not from the API server, so it arrives as a server error with a kubelet's message
inside it rather than as a Forbidden.
Step 3 is the measurement the second disagreement asks for, taken in the only direction a v1.35
cluster can take it. The gate is now in featureGates and the kubelet restarted cleanly, and
?query=kubelet still fails. On this release the gate is necessary and not sufficient, exactly as
system-logs.md:246-248 says. What that does not tell you is whether :250-253 is right about
v1.36, where the gate is no longer a variable; the release announcement at
kubernetes-v1-36-release/index.md:227 says enableSystemLogQuery is still required there, the
concept page's second paragraph reads as though it is not, and nothing on this node can arbitrate
between them.
Step 4 is the post working. ?query=kubelet returns journald lines for the kubelet unit — several
hundred to a few thousand, depending on how long the guest has been up — and the count from wc -l
is the whole unit log, because system-logs.md:300 says tailLines defaults to fetching the whole
log. This is the post's :61 with a real node name, and it has not changed in three years and five
months.
Step 5 produces three counts that should not all match. The case-sensitive grep -c 'error' count
is the smallest. The pattern=error count should equal or exceed the case-insensitive grep -c -i
count rather than the case-sensitive one, because journalctl treats an all-lowercase pattern as
case-insensitive — behaviour that system-logs.md:296 does not mention, since its entire
documentation of pattern is the eleven words pattern filters log entries by the provided
PERL-compatible regular expression. pattern=Error carries an uppercase letter and should drop
back to case-sensitive matching. The finding is not the numbers; it is that a one-line table cell is
the complete reference for a filter whose semantics you can only discover by measuring them.
Step 6 walks the four options the post never names. tailLines=5 returns five lines. boot=0 is
the current boot and matches the unfiltered count. boot=-1 asks for the previous boot and will
usually return nothing on a guest that has been rebooted once or not at all — journald on a fresh
Debian guest keeps a volatile journal unless /var/log/journal exists, so previous boots may not be
retained at all, which is a property of the node and not of this feature. sinceTime with an
RFC3339 timestamp ten minutes back returns a strict subset of the unfiltered count, and on a node
where you have just restarted the kubelet twice it will not be a small subset.
Step 7 walks the heuristic in all three of its branches. ?query=madeup finds no journald unit by
that name, falls through to the file checks, and returns the three lines you wrote — that is
/var/log/<servicename>/<servicename>.log, the third and last branch of the post's heuristic at
:33-34. ?query=/madeup/madeup.log returns the same three lines by the explicit path form the
post prints at :72, and the leading slash is what distinguishes the two. ?query=containerd
returns journald lines, because containerd is a real unit on this node and the native logger is
checked first. ?query=nosuchservice matches no unit and no file under any of the three names, and
fails.
Step 8 settles the post's :39-40 in the post's favour, for the hop the reader can see. With only
get on nodes/proxy, auth can-i says yes for nodes/proxy and no for nodes/log, and the
impersonated request succeeds — so the API server authorizes this path on proxy, and get alone
is enough. Nothing in any of the post's three commands is an HTTP POST, so the create half of
:39-40 is never exercised by anything the post asks you to do; kubelet-authn-authz.md:56-64 maps
create to POST and the post issues none. With the binding swapped to nodes/log, the same request
is refused, which proves the API server never consults the subresource that the page the post links
assigns to /logs/\*.
Step 9 shows the hop the reader cannot see. The kubelet's own configuration has authorization.mode:
Webhook, so it does re-authorize, and the API server reaches it holding
apiserver-kubelet-client.crt. system:kubelet-api-admin lists nodes/log among its resources —
that ClusterRole exists for the second hop, and nothing on this cluster is bound to it. The
certificate subject prints O = system:masters, which means the identity crossing the second hop
bypasses RBAC entirely, which is why the /logs/\* → nodes/log row at kubelet-authn-authz.md:72
never bites here and why the post could collapse two authorizations into one clause without anybody
noticing. That also settles certificates.md:119 against the note at :122-125: on a kubeadm
cluster at this version the table row is describing what actually happens and the note is describing
a different certificate.
Step 10, in order: the concept page's two adjacent paragraphs that disagree about whether
enableSystemLogQuery survives v1.36; the generated reference that disagrees with it about
enableSystemLogHandler, together with the security note the concept page compresses into one
clause; both authorization tables, printed back to back though forty lines separate them in the
file, agreeing with each other and with neither the post's sentence nor your step 8; the certificate
table and the note under it; the one line of the v1.36 announcement that is the third statement
about the recipe; and then the two greps that carry the never absorbed finding — Get-WinEvent in
one file, and node-log-query-alpha in one file, both of them this post. The gate file is
twenty-one lines with no removed key and no locked key.
Read on
-
system-logs.md:286-300— the heuristic and all six query options in one screen. Read the table's descriptions against what step 6 measured;queryis the only one marked (required), and it is the only one the post uses. -
kubelet-authn-authz.md:99-134— fine-grained authorization in full.KubeletFineGrainedAuthzis beta and on by default at v1.35 and stable at v1.36, and it is what splits/pods,/healthzand/configzout of theproxycatch-all./logs/\*was never in that catch-all, which is why this section changes nothing for this feature and everything for the rest of the kubelet API. -
certificates.md:113-126— the whole certificate table and the note beneath it. It is worth reading for the general shape as much as for the disagreement: one row of that table is the reason the second authorization hop in step 9 has no visible effect. -
kubelet-config.v1beta1.md:1583-1599— the generated reference for both options. It carries the sentence the concept page does not: enabling the query feature has security implications and the recommendation is to enable it on a need basis for debugging and disable it otherwise. That is the same advicesystem-logs.md:255-256attaches to the other option. -
Unanswerable from the pin: whether
/logs/\*ever mapped tonodes/proxy. The post saysnodes/proxyat:39-40and the pin's tables saynodes/log, and both of the pin's tables are current-state documents with no history in them. The post's own:49records that the warning block was added in early 2026 without the sentence above it being touched, so an editor read that paragraph three years after publication and changed something else — but the pin cannot say whether the sentence was right when written, only that the page it now links disagrees with it.
Teardown — sudo mv /var/lib/kubelet/config.yaml.bak /var/lib/kubelet/config.yaml;
sudo systemctl restart kubelet; sleep 10; systemctl is-active kubelet; kubectl delete
clusterrolebinding logreader-bind; kubectl delete clusterrole node-proxy-get node-log-get; kubectl
delete sa logreader; sudo rm -rf /var/log/madeup; rm -f /tmp/kubelet-all.log. Restore the kubelet
configuration before anything else: a node left with enableSystemLogQuery: true serves every
service log on it to anyone who can reach the node proxy endpoint, which is the posture
kubelet-config.v1beta1.md:1597-1598 tells you not to leave behind. Then
tear the guest down.