We use LDAP for a single source of truth for users and service accounts, via nitnelave/lldap
We will use this for:
- Keycloak, including Traefik forward authentication. This will let us put any Kubernasty service behind a login prompt.
- Some services which can talk to LDAP directly, like Gitea.
About this deployment #
Specifics of LDAP vary widely.
The mostly widely used LDAP deployment type is Active Directory; keep in mind you may need to do some translation of other docs to make use of them.
Prerequisite TLS certificate #
We will generate a self-signed certificate for the LLDAP server
- LDAP connections are unencrypted by default
- The LDAP server contains passwords, which are pretty important to keep secure
- Kubernetes does not encrypt traffic between nodes by default
- You can use a certificate authority to generate certs for the LDAP server
- But then you have to maintain a certificate authority
- The benefit of a certificate authority is that the CA can sign new certs, which clients will accept the same as the original cert
- In our cluster, if we need to change the cert, we will just reference the new copy in new deployments
- We won’t let outside clients connect directly to the LDAP server (it’ll just be accessible inside the cluster), so we don’t need a CA.
- This is upgradeable to a real CA in the future with the same level of effort as just deploying a new self-signed cert
- It means we don’t have to learn about and configure encrypted Kubernetes network fabric now
You cannot use ECDSA keys for LDAP – they must be RSA.
The server may work with them, but clients like
ldapwhoami
don’t.Not sure if clients we care about, like Keycloak and Gitea, would work or not.
Just using RSA to be safe.
# Some tunable options
# 7300 days is 20 years, ur cluster won't last 20 weeks u piece of shit
validdays=7300
# The simple hostname for the container in your cluster
svchostname=lldap
# The namespace the container will be running in
svcnamespace=lldap
# I am not sure if there are constraints on the subject; something like this is typical:
certsubj="/C=US/ST=TX/O=Kubernasty LDAP Service/CN=$svchostname.$svcnamespace"
# Generate an RSA key
# ECDSA does NOT work!
openssl req -newkey rsa:4096 -x509 -nodes \
-out lldap.crt.pem \
-keyout lldap.key.pem \
-days "$validdays" \
-subj "$certsubj" \
-addext "subjectAltName = DNS:$svchostname,DNS:$svchostname.$svcnamespace,DNS:$svchostname.$svcnamespace.svc.cluster.local"
# Verify that the common name and subject alt names are as intended
openssl x509 -noout -text -in lldap.crt.pem | less
cat lldap.key.pem lldap.crt.pem > lldap.combined.pem
gopass insert -m kubernasty/lldap.crt.pem < lldap.crt.pem
gopass insert -m kubernasty/lldap.key.pem < lldap.key.pem
gopass insert -m kubernasty/lldap.combined.pem < lldap.combined.pem
Deploying lldap #
Create the various manifest files under
♘
kubernasty/manifests/crust/lldap
.
Create lldap-credentials.secret.yaml
#
The admin username will be 0p3r4t0r
, and the password we generate below.
lldapJwtSecret="$(pwgen 64)"
gopass generate kubernasty/lldap-0p3r4t0r-pw
lldapLdapUserPass="$(gopass cat kubernasty/lldap-0p3r4t0r-pw)"
cat > kubernasty/manifests/crust/lldap/secrets/lldap-credentials.secret.yaml <<EOF
apiVersion: v1
kind: Secret
metadata:
name: lldap-credentials
namespace: lldap
type: generic
stringData:
lldapJwtSecret: $lldapJwtSecret
lldapLdapUserPass: $lldapLdapUserPass
EOF
sops --encrypt --in-place manifests/crust/lldap/secrets/lldap-credentials.secret.yaml
Create lldap-tls.secret.yaml
and lldap-cert.configmap.yaml
#
key="$(gopass -n kubernasty/lldap.key.pem | base64 -w 0)"
certificate="$(gopass -n kubernasty/lldap.crt.pem | base64 -w 0)"
cat > manifests/crust/lldap/secrets/lldap-tls.secret.yaml <<EOF
apiVersion: v1
kind: Secret
metadata:
name: lldap-tls
namespace: lldap
type: generic
data:
key: $key
certificate: $certificate
EOF
sops --encrypt --in-place manifests/crust/lldap/secrets/lldap-tls.secret.yaml
cat > manifests/crust/lldap/configmaps/lldap-cert.configmap.yaml <<EOF
apiVersion: v1
kind: ConfigMap
metadata:
name: lldap-cert
namespace: lldap
binaryData:
certificate: $certificate
EOF
Create the other manifests #
- Note that what’s there in
♘
kubernasty/manifests/crust/lldap
has some hardcoded values like my DNS and x509 names. - The container users unprivileged ports 3890/6360 for LDAP/LDAPS by default, rather than the well-known but privileged values of 389/636. It also uses 17170 for its web UI port. However, we can make the Kubernetes service listen in the well-known ports.
Deploy #
Commit and push, and Flux will deploy OpenLDAP automatically.
Log in #
How can we log in to it? The LDAP service isn’t exposed to the network.
Log in on the command line from an ephemeral container #
One way is to create an ephemeral container and install LDAP clients in it. From your kubectl client machine:
kubectl get pods -n lldap
# Returning e.g.:
# NAME READY STATUS RESTARTS AGE
# lldap-56f79f9b57-8mrw7 1/1 Running 0 12m
kubectl debug -it lldap-56f79f9b57-8mrw7 --image=alpine:latest --target=lldap --namespace=lldap
# Now you will be in a shell in a new ephemeral container
From that shell we can:
apk add openldap-clients
nslookup lldap
# Server: 10.43.0.10
# Address: 10.43.0.10:53
# Name: lldap.lldap.svc.cluster.local
# Address: 10.43.96.231
# ...
# Try to query the LDAP server anonymously - this should fail
ldapwhoami -x -H ldap://lldap:389
# Authenticate and query the ldap server as the admin user
admindn="cn=0p3r4t0r,ou=people,dc=kubernasty,dc=micahrl,dc=com"
adminpw="adminp@ssw0rd"
ldapsearch -x -H ldap://lldap:389 -D "$admindn" -w "$adminpw" -b ou=people,dc=kubernasty,dc=micahrl,dc=com -s sub '(objectClass=*)' 'givenName=username*'
# ... should list the admin user
# To test connecting over TLS, you have to copy the certificate to the ephemeral container
# You catn just cat >/cert.pem and paste it into your terminal.
# Once that's done:
export LDAPTLS_CACERT=/cert.pem
ldapsearch -x -H ldaps://lldap:636 -D "$admindn" -w "$adminpw" -b ou=people,dc=kubernasty,dc=micahrl,dc=com -s sub '(objectClass=*)' 'givenName=username*'
# ... should list the admin user again
Configure cluster users #
We need a few users that we can reference elsewhere.
authenticator
user.gopass generate kubernasty/lldap/authenticator-user 64
. Create in the UI, and add tolldap_strict_readonly
. This account will be used to bind to LDAP and authenticate end users. TODO: replace the dedicatedgitea
LLDAP user with this one.
Troubleshooting #
- Older LDAP commands like
ldapsearch
will give a generic error likeldap_sasl_bind(SIMPLE): Can't contact LDAP server (-1)
if they can’t authenticate the TLS certificate forldaps://
. Differentiate between no network connectivity to the lldap service and an untrusted CA cert byexec
ing into a container and running a command likeopenssl s_client -connect lldap:636
. If it shows your certificate, the host has network access. (We use this method because ping is blocked(?) and you can’t talk to the TLS service directly over netcat.)
Todo #
- TODO: Change LLDAP structure to match DNS.
When I did this originally, I was using DNS of
kubernasty.micahrl.com
, and so I made the equivalent LDAP structure ofdc=kubernasty,dc=micahrl,dc=com
. I have now moved tomicahrl.me
, but kept the LDAP structure the same. - TODO: Create self-signed LLDAP cert with cert-manager. I created a self-signed cert automatically, but cert-manager can do this for me.