Anyone who doesn’t know what is NTP (Network Time Protocol), directly from Wikipedia,
“The Network Time Protocol is a networking protocol for clock synchronization between computer systems over packet-switched, variable-latency data networks.”
In this blog I am using OpenNTPD which is a FREE implementation of NTP. It provides
- the ability to sync the local clock to remote NTP servers
- can act as NTP server itself, redistributing the local clock.
Problem Description
I was curious if it is possible to run NTPD using openntpd in the Kubernetes cluster. Found a handy blog. In this blog, the author is showing how you can run NTP in a container. So I am going to follow this blog to create a build process for NTPD docker image and use that image to run NTPD in the Kubernetes cluster.
Solutions
Test with Docker container
First, create a Dockerfile using the following content.
FROM alpine
RUN apk update
RUN apk add openntpd
ENTRYPOINT ["ntpd"]
Now you can build docker image using the following command
docker build -t goglides/ntpd .
Let’s test it using the following
docker run goglides/ntpd -d
Output:
ntpd: can't set priority: Permission denied
reset adjtime failed: Operation not permitted
creating new /var/db/ntpd.drift
adjtimex failed: Operation not permitted
ntp engine ready
constraint request to 172.217.9.196
constraint request to 2607:f8b0:4004:806::2004
tls connect failed: 2607:f8b0:4004:806::2004 (www.google.com): connect: Address not available
no constraint reply from 2607:f8b0:4004:806::2004 received in time, next query 900s
tls connect failed: 172.217.9.196 (www.google.com): ssl verify memory setup failure
no constraint reply from 172.217.9.196 received in time, next query 900s
Apparently, our container is not working as expected, if you look at following errors,
ntpd: can't set priority: Permission denied
reset adjtime failed: Operation not permitted
creating new /var/db/ntpd.drift
adjtimex failed: Operation not permitted
which is basically saying not enough permission is because docker is not able to run binary, we can fix this by adding --privileged flag as follows
docker run --privileged goglides/ntpd -d
Output:
creating new /var/db/ntpd.drift
ntp engine ready
constraint request to 2607:f8b0:4004:806::2004
constraint request to 172.217.9.196
tls connect failed: 2607:f8b0:4004:806::2004 (www.google.com): connect: Address not available
no constraint reply from 2607:f8b0:4004:806::2004 received in time, next query 900s
tls connect failed: 172.217.9.196 (www.google.com): ssl verify memory setup failure
no constraint reply from 172.217.9.196 received in time, next query 900s
And for SSL issue I keep seeing it, I tried adding following in Dockerfile which didn’t fix the issue,
RUN apk add ca-certificates
RUN update-ca-certificates
There is an open bug in the official repo, https://gitlab.alpinelinux.org/alpine/aports/issues/9635 which mentioning why we are seeing this issue.
The main cause is mentioned below (copy/pasted from the ticket)
This happens because of the chroot(2) call in ntpd(8) child. ntpd(8) calls open(“/tmp/libtlscompat***”, …) after chrooting to /var/empty and fails because there is no /var/empty/tmp. Doing mkdir -m 1777 /var/empty/tmp helps, but I don’t know if this is the right approach. UPD 1: There is a way to specify the directory where ntpd(8) should chroot with ./configure … — with-privsep-path=, so it would make sense to create /var/lib/openntpd with tmp inside owned by ntp:ntp. UPD 2: Actually, there is no call to open(“/tmp/libtls***”, …) on OpenBSD. I guess that this is standalone-only thing. UPD 3: The offending function SSL_CTX_load_verify_mem resides here. Calling mkstemp(3) from there happens after chrooting and ntpd(8) does not expect that since vanilla libressl does all this in memory:
/* From libressl-2.9.1 */
int
SSL_CTX_load_verify_mem(SSL_CTX *ctx, void *buf, int len)
{
return (X509_STORE_load_mem(ctx->cert_store, buf, len));
}
UPD 4: OpenNTPD does not use — with-privsep-path=in code, it just for hints. Instead it chroots to the home directory of a running user, so it’s necessary to setup a dedicated user (e.g. openntpd) with a home directory pointing to e.g. /var/lib/openntpd.
So solution is to add mkdir -m 1777 /var/empty/tmp as workaround for now. My final Dockerfile looks like this.
FROM alpine:3.11.3
RUN apk update
RUN apk add openntpd
RUN mkdir -m 1777 /var/empty/tmp
ENTRYPOINT ["ntpd"]
Now I am seeing following output
docker run --privileged goglides/ntpd -d
Output:
creating new /var/db/ntpd.drift
ntp engine ready
constraint request to 172.217.9.196
constraint request to 2607:f8b0:4004:806::2004
tls connect failed: 2607:f8b0:4004:806::2004 (www.google.com): connect: Address not available
no constraint reply from 2607:f8b0:4004:806::2004 received in time, next query 900s
constraint reply from 172.217.9.196: offset -1.288009
reply from 66.228.58.20: offset -0.292780 delay 0.079884, next query 8s
reply from 72.87.88.202: offset -0.299418 delay 0.088550, next query 6s
reply from 138.68.201.49: offset -0.288903 delay 0.137101, next query 9s
The next step is to provide a configuration file. I am simply going to create /tmp/ntpd.conf conf file with servers pool.ntp.org content and mount that to the container. For detail configuration options, please visit ntpd.conf man page
docker run -v /tmp/ntpd.conf:/etc/ntpd.conf --privileged goglides/ntpd -d -f /etc/ntpd.conf
Output:
creating new /var/db/ntpd.drift
ntp engine ready
reply from 69.89.207.99: offset -0.356652 delay 0.122182, next query 33s
reply from 96.8.121.205: offset -0.356918 delay 0.153844, next query 30s
reply from 138.236.128.112: offset -0.346649 delay 0.108738, next query 30s
reply from 96.8.121.205: offset -0.359319 delay 0.153514, next query 34s
reply from 204.11.201.10: offset -0.359962 delay 0.152213, next query 32s
reply from 69.89.207.99: offset -0.362657 delay 0.121926, next query 32s
adjusting local clock by 0.064548s
Alright our test is complete let’s run this container in the background with following
docker run --name ntpd \
-v /tmp/ntpd.conf:/etc/ntpd.conf \
--restart=always -d --privileged \
goglides/ntpd -d -s -f /etc/ntpd.conf
The -s means "Try to set the time immediately at startup." You can check if the process is running or not using following,
docker ps
Output:
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
035cf608e489 goglides/ntpd "ntpd -d -s -f /etc/…" About a minute ago Up About a minute ntpd
and logs
docker logs ntpd
Output:
creating new /var/db/ntpd.drift
adjtimex adjusted frequency by 0.000000ppm
ntp engine ready
reply from 45.55.217.50: offset -0.395447 delay 0.139999, next query 7s
reply from 50.205.244.112: offset -0.392487 delay 0.139775, next query 6s
set local clock to Tue Mar 3 20:18:48 UTC 2020 (offset -0.395447s)
reply from 216.240.36.24: negative delay -0.247795s, next query 3217s
reply from 23.129.64.159: negative delay -0.245678s, next query 3019s
reply from 50.205.244.112: offset 0.024988 delay 0.082805, next query 5s
reply from 45.55.217.50: offset 0.019650 delay 0.093784, next query 7s
reply from 45.55.217.50: offset 0.009099 delay 0.085599, next query 9s
peer 45.55.217.50 now valid
Deploy to Kubernetes cluster
Since we validate NTP is working in a container, I should able to deploy this in the Kubernetes cluster easily. For this, I have to modify Dockerfile little bit to address some of the challenges.
- Run container in Foreground We can easily achieve this by changing our docker entrypoint, by adding -d flag. As per man page, -d means do not daemonize. If this option is specified, ntpd will run in the foreground and log to stderr.
- Supply configuration file I am thinking about supplying configuration using configmap. I have to modify Dockerfile to access file config using -f flag. After that create configmap with actual config, mount the volume and use that custom configuration.
For this created a custom entrypoint.sh to handle some logic.
#! /bin/sh
if [-z "${NTP_CONF_FILE}"]
then
NTP_CONF_FILE="/etc/ntpd.conf"
fi
ntpd -v -d -s -f ${NTP_CONF_FILE}
and modified Dockerfile to consume this.
FROM alpine:3.11.3
RUN apk update
RUN apk add openntpd
RUN mkdir -m 1777 /var/empty/tmp
ADD ./entrypoint.sh ./entrypoint.sh
RUN chmod 755 ./entrypoint.sh
ENTRYPOINT ["./entrypoint.sh"]
After that, I created a Kubernetes DaemonSet object to use this docker image. Save following content to ds-ntpd.yaml
apiVersion: v1
kind: ConfigMap
metadata:
name: ntpd-config
data:
ntpd.conf: |
servers pool.ntp.org
---
apiVersion: apps/v1
kind: DaemonSet
metadata:
name: ntpd
labels:
k8s-app: ntpd
created-by: tech.goglides.com
spec:
selector:
matchLabels:
name: ntpd
template:
metadata:
labels:
name: ntpd
spec:
tolerations:
- key: node-role.kubernetes.io/master
effect: NoSchedule
containers:
- name: ntp-sync
image: goglides/ntpd
imagePullPolicy: Never
resources:
limits:
memory: 20Mi
cpu: 20m
requests:
cpu: 10m
memory: 10Mi
securityContext:
privileged: true
env:
- name: NTP_CONF_FILE
value: /app/ntpd.conf
volumeMounts:
- name: ntpd-config
mountPath: /app/
volumes:
- name: ntpd-config
configMap:
name: ntpd-config
Now finally apply this manifest to Kubernetes using the following,
kubectl apply -f ds-ntpd.yaml
Output:
configmap/ntpd-config created
daemonset.apps/ntpd created
You can verify if pods are running or not using following,
kubectl get pods
Output:
NAME READY STATUS RESTARTS AGE
ntpd-b7csn 1/1 Running 0 68s
And check logs,
kubectl logs ntpd-b7csn
Output:
creating new /var/db/ntpd.drift
adjtimex adjusted frequency by 0.000000ppm
ntp engine ready
reply from 51.158.147.92: offset -0.715358 delay 0.126846, next query 6s
reply from 176.9.40.131: offset -0.722062 delay 0.148438, next query 9s
reply from 5.186.65.2: offset -0.716915 delay 0.148267, next query 7s
reply from 212.25.15.128: offset -0.703432 delay 0.160469, next query 8s
set local clock to Tue Mar 3 23:09:38 UTC 2020 (offset -0.715358s)
Top comments (0)