Fake it 'till you make it

When Ubuntu 22.04 LTS was released, I wanted to re-image my Kubernetes cluster nodes so they are in a clean state. Unfortunately, this comes with loosing the „age“ (the duration of cluster membership) for each node.

So I set out to find where this value comes from and if it's possible to tamper with it.

TL;DR Yes, it is. 🤓


Lets see how far we can get with kubectl itself.


# print nodes and their age as a table
kubectl get nodes

Editing a node (kubectl edit nodes <nodeName>) shows the field we are looking for is under the path .metadata.creationTimestamp, but changing it has no effect.

# print a node's creationTimestamp
kubectl get nodes <nodeName> -o json | jq ".metadata.creationTimestamp"

Lets describe our node.metadata.creationTimestamp field:

$ kubectl explain node.metadata.creationTimestamp
KIND:     Node

FIELD:    creationTimestamp <string>

     CreationTimestamp is a timestamp representing the server time when this
     object was created. It is not guaranteed to be set in happens-before order
     across separate operations. Clients may not set this value. It is
     represented in RFC3339 form and is in UTC.

     Populated by the system. Read-only. Null for lists. More info:

     Time is a wrapper around time.Time which supports correct marshaling to
     YAML and JSON. Wrappers are provided for many of the factory methods that
     the time package offers.

Important detail to remember from here:

Populated by the system. Read-only.

API documentation

Since kubectl explain told us to look at the Node v1 API object, we can see it includes ObjectMeta which should hold our creationTimestamp. Alright!


What is etcd?

As the primary datastore of Kubernetes, etcd stores and replicates all Kubernetes cluster states.

I knew etcd holds all relevant information. So I started to familiarize with it's utilities:


# lets use etcd API version 3
export ETCDCTL_API=3
# pass certs/keys and endpoints to talk to
export ETCD_ARGS="\
--cacert /etc/kubernetes/pki/etcd/ca.crt \
--cert /etc/kubernetes/pki/etcd/peer.crt \
--key /etc/kubernetes/pki/etcd/peer.key \
--endpoints https://localhost:2379"
# list all keys stored in etcd
etcdctl $ETCD_ARGS get / --prefix --keys-only

After being successfully connected the question is: what are we looking for?

🍌 Minions 🍌

Listing all keys for a control node gave this:

$ etcdctl $ETCD_ARGS get / --prefix --keys-only | grep <nodeName>

Listing all keys for a worker node gave that:

$ etcdctl $ETCD_ARGS get / --prefix --keys-only | grep <nodeName>

Looking at each record it was clear the node parameters are stored under /registry/minions/<nodeName>.

etcdctl $ETCD_ARGS get "/registry/minions/<nodeName>"

The output of the above command includes a lot of garbled (binary) data, lets apply some json magic:

# print json representation of the etcd record
etcdctl $ETCD_ARGS get "/registry/minions/<nodeName>" -w json
# print json representation of the etcd record and decodes the actual value
etcdctl $ETCD_ARGS get "/registry/minions/<nodeName>" -w json | jq -r ".kvs[0].value" | base64 -d

Meh, that's still binary data and no timestamp in sight. Lets see, how kube-etcd-helper might help us here.


--cacert /etc/kubernetes/pki/etcd/ca.crt \
--cert /etc/kubernetes/pki/etcd/peer.crt \
--key /etc/kubernetes/pki/etcd/peer.key \
--endpoint https://localhost:2379"
# get value as json
kube-etcd-helper $ETCD_HELPER_ARGS get "/registry/minions/<nodeName>"
# get value as json and parse it with jq
kube-etcd-helper $ETCD_HELPER_ARGS get "/registry/minions/<nodeName>" | tail -n +2 | jq
  "metadata": {
    "annotations": {
      "kubeadm.alpha.kubernetes.io/cri-socket": "unix:///var/run/containerd/containerd.sock",
      "node.alpha.kubernetes.io/ttl": "0",
      "volumes.kubernetes.io/controller-managed-attach-detach": "true"
    "creationTimestamp": "2021-03-12T21:48:27Z",
    "labels": {
      "beta.kubernetes.io/arch": "arm64",
      "beta.kubernetes.io/os": "linux",
      "kubernetes.io/arch": "arm64",
      "kubernetes.io/hostname": "k8s-control-01",
      "kubernetes.io/os": "linux",
      "node-role.kubernetes.io/control-plane": "",
      "node.kubernetes.io/exclude-from-external-load-balancers": ""

Yes! Finally we found a data representation that is somewhat readable. So kube-etcd-helper knows how to decode that binary data. But unfortunately, it does not have the ability to put things back into etcd.


With python-etcd3 you can easily retrieve a record, do a search/replace and put it back into etcd.

#!/usr/bin/env python3
"""Demo python-etcd3."""
import etcd3

CA_CERT = "/etc/kubernetes/pki/etcd/ca.crt"
CERT_CERT = "/etc/kubernetes/pki/etcd/peer.crt"
CERT_KEY = "/etc/kubernetes/pki/etcd/peer.key"

def main():
    """Provide main routine."""
    k = "/registry/minions/<nodeName>"
    c = etcd3.client(ca_cert=CA_CERT, cert_cert=CERT_CERT, cert_key=CERT_KEY)
    v, _ = c.get(k) # retrieve a record
    old = b"\x08\xf3\xfe\x81\x99\x06\x10\x00"  # 2022-09-13 13:02:43+00:00
    new = b"\x08\xab\xc3\xaf\x82\x06\x10\x00"  # 2021-03-12 21:48:27+00:00
    v.replace(old, new, 1) # search/replace
    c.put(k, v) # put it back

if __name__ == "__main__":

Awesome! But what's this gibberish about?

Poking around

I have created some disposable objects with similar content and compared their resulting etcd records. Soon I could spot where to look for the creationTimestamp.

kubectl create configmap my-config1 --from-literal=key1=config1 && \
sleep 1 && \
kubectl create configmap my-config2 --from-literal=key2=config2

Then I tried to use one value for the other configmap and soon I made to actually replace it successfully. But I could not yet calculate those values yet... Googling around brought my attention to these files, where message Node and message ObjectMeta were defined: - https://github.com/kubernetes/api/blob/master/core/v1/generated.proto - https://github.com/kubernetes/apimachinery/blob/master/pkg/apis/meta/v1/generated.proto

Entering the world of Protobuf...


I have never used Protobuf before so I started with reading the documentation. Soon I played with my own first .proto files and some Python. I copied the Time definition from the Kubernetes project:

syntax = "proto2";

package k8s.io.apimachinery.pkg.apis.meta.v1;

message Time {
    // Represents seconds of UTC time since Unix epoch
    // 1970-01-01T00:00:00Z. Must be from 0001-01-01T00:00:00Z to
    // 9999-12-31T23:59:59Z inclusive.
    optional int64 seconds = 1;
    // Non-negative fractions of a second at nanosecond resolution. Negative
    // second values with fractions must still have non-negative nanos values
    // that count forward in time. Must be from 0 to 999,999,999
    // inclusive. This field may be limited in precision depending on context.
    optional int32 nanos = 2;

After compiling this definition I could finally create my own creationTimestamp using Python:

protoc --python_out=./ objectmeta.proto
#!/usr/bin/env python3
"""Demo protobuf."""

import objectmeta_pb2

def main():
    """Provide main routine."""
    fake = objectmeta_pb2.Time()
    fake.seconds = 1615585707  # 2021-03-12 21:48:27+00:00
    fake.nanos = 0
    fake_data = fake.SerializeToString()

if __name__ == "__main__":

The output of this script will be:


Now we can use this script to generate timestamps as we like and put these values into etcd using python-etcd3.