Friends Don’t Let Friends Use HTTP – Enforcing TLS End-to-End

Duration: ~20 minutes

Overview

Start with a running application on HTTP. Use cert-manager to issue a certificate for that hostname. Put the certificate on the Route and access the app again over HTTPS with redirect.

This cluster already has cert-manager. You will use the existing selfsigned ClusterIssuer for the lab certificate. Leave the ACME ingress and RHACS certificates alone.

Why it matters

Plain HTTP leaves credentials, cookies, and API bodies open on the wire. TLS encrypts the path and, when the certificate is signed by a CA the client trusts, proves the server identity. Redirecting HTTP to HTTPS stops casual downgrade to cleartext.

What does it solve

  • Credential theft and session hijacking on the wire

  • Clients that keep using http:// after you enable TLS

  • Manual certificate copy-paste with no renewal story

  • Confusion between encryption (-k) and identity (a trusted CA)

Your Mission

Stand up an app, prove HTTP is readable, issue a certificate with cert-manager, then hit the same hostname again over HTTPS. Complete Part A, then Part B, then Part C.

Click each step only if you need a hint.

Part A: Start with an application

Deploy a UBI httpd app, expose it on HTTP, and capture the Route hostname. That hostname is the name you will put on the certificate.

Create project 101-12-tls

This lab is self-contained. It does not reuse the 101-11 rebuild project. Stay in this project for every later step.

oc new-project 101-12-tls || oc project 101-12-tls
oc project should print Now using project "101-12-tls".
Deploy UBI httpd as webapp

Use the OpenShift-ready UBI httpd image on port 8080. Wait until the rollout finishes before you expose it.

oc create deployment webapp --image=registry.access.redhat.com/ubi9/httpd-24
oc expose deployment webapp --port=8080 --target-port=8080
oc rollout status deployment/webapp --timeout=120s
Rollout status should end with successfully rolled out.
Create an HTTP Route for webapp

oc expose service creates an HTTP Route. Copy the hostname; Part B puts that exact name on the certificate.

oc expose service webapp
ROUTE=$(oc get route webapp -o jsonpath='{.spec.host}')
echo "$ROUTE"
The echo should be a hostname, not empty. If you open a new terminal later, run oc project 101-12-tls and set ROUTE again.
Curl the HTTP Route

Confirm the page loads. Anyone on-path can read this response.

ROUTE=$(oc get route webapp -o jsonpath='{.spec.host}')
curl -sI "http://${ROUTE}" | head -10
curl -s "http://${ROUTE}" | head -5
You should see HTTP 200 and HTML. There is no TLS yet.

Part B: Issue a certificate with cert-manager

cert-manager watches Certificate objects, talks to an Issuer, and writes a TLS Secret. This cluster already has issuers:

Issuer What it is

selfsigned ClusterIssuer

Signs with its own key. No public trust. You will use this.

acme-bifrost-production-ddns (and fallback)

ACME (public CA). Already used for the cluster ingress certificate.

Do not edit or delete those ClusterIssuers. Do not change certificates in openshift-ingress, openshift-config, cert-manager, or stackrox.

List ClusterIssuers

Confirm selfsigned and the ACME issuers are Ready.

oc get clusterissuer
selfsigned and acme-bifrost-production-ddns should show True.
Inspect the cluster ingress certificate

This is a publicly signed certificate. Your lab certificate later is signed only by a CA you create.

oc get certificate -n openshift-ingress cert-manager-ingress-cert
oc get secret cert-manager-ingress-cert -n openshift-ingress -o jsonpath='{.data.tls\.crt}' \
  | base64 -d | openssl x509 -noout -issuer -subject -dates
The issuer should be a public CA (ACME / Let’s Encrypt style). Browsers already trust that. They will not trust your lab CA until you pass --cacert.
Create the lab CA certificate

Mint a CA in your namespace from the cluster selfsigned ClusterIssuer. Do not change that ClusterIssuer.

oc project 101-12-tls
oc apply -f - <<'EOF'
apiVersion: cert-manager.io/v1
kind: Certificate
metadata:
  name: lab-ca
  namespace: 101-12-tls
spec:
  isCA: true
  commonName: 101-12-lab-ca
  secretName: lab-ca-tls
  issuerRef:
    name: selfsigned
    kind: ClusterIssuer
EOF
oc apply should report certificate.cert-manager.io/lab-ca created (or configured).
Wait until lab-ca is Ready

cert-manager writes Secret lab-ca-tls when the CA is Ready.

oc wait --for=condition=Ready certificate/lab-ca -n 101-12-tls --timeout=90s
oc get certificate lab-ca -n 101-12-tls
READY should be True.
Create the namespaced CA Issuer

This Issuer signs leaf certificates using Secret lab-ca-tls. It lives only in 101-12-tls.

oc apply -f - <<'EOF'
apiVersion: cert-manager.io/v1
kind: Issuer
metadata:
  name: lab-ca
  namespace: 101-12-tls
spec:
  ca:
    secretName: lab-ca-tls
EOF
issuer.cert-manager.io/lab-ca created (or configured).
Issue the webapp certificate for the Route hostname

The SAN must match the HTTP Route hostname exactly. Recapture $ROUTE if your shell lost it.

oc project 101-12-tls
ROUTE=$(oc get route webapp -o jsonpath='{.spec.host}')
echo "Certificate will be issued for: ${ROUTE}"
oc apply -f - <<EOF
apiVersion: cert-manager.io/v1
kind: Certificate
metadata:
  name: webapp
  namespace: 101-12-tls
spec:
  secretName: webapp-tls
  issuerRef:
    name: lab-ca
    kind: Issuer
  dnsNames:
  - ${ROUTE}
EOF
Echo should show the same hostname as Part A. oc apply should create certificate.cert-manager.io/webapp.
Confirm both certificates are Ready

webapp-tls holds the leaf cert and key. lab-ca-tls is the CA you will pass to curl --cacert.

oc wait --for=condition=Ready certificate/webapp -n 101-12-tls --timeout=90s
oc get certificate,issuer -n 101-12-tls
oc get secret webapp-tls lab-ca-tls -n 101-12-tls
Both certificates READY True. Both Secrets exist.

Part C: Access the application again

Replace the HTTP Route with edge TLS that uses the cert-manager Secret. Then curl HTTPS with the lab CA, and confirm HTTP redirects.

Extract the cert, key, and CA to /tmp/101-12-certs

Keep the same hostname so the SAN still matches. Write PEM files the oc create route command can read.

oc project 101-12-tls
ROUTE=$(oc get route webapp -o jsonpath='{.spec.host}')
mkdir -p /tmp/101-12-certs
oc get secret webapp-tls -n 101-12-tls -o jsonpath='{.data.tls\.crt}' | base64 -d > /tmp/101-12-certs/tls.crt
oc get secret webapp-tls -n 101-12-tls -o jsonpath='{.data.tls\.key}' | base64 -d > /tmp/101-12-certs/tls.key
oc get secret lab-ca-tls -n 101-12-tls -o jsonpath='{.data.tls\.crt}' | base64 -d > /tmp/101-12-certs/ca.crt
ls /tmp/101-12-certs should show tls.crt, tls.key, and ca.crt.
Replace the HTTP Route with edge TLS and redirect

Delete the HTTP Route, then create an edge Route on the same hostname with the extracted PEM files.

ROUTE=$(oc get route webapp -o jsonpath='{.spec.host}')
oc delete route webapp
oc create route edge webapp --service=webapp --port=8080 \
  --hostname="${ROUTE}" \
  --cert=/tmp/101-12-certs/tls.crt \
  --key=/tmp/101-12-certs/tls.key \
  --ca-cert=/tmp/101-12-certs/ca.crt \
  --insecure-policy=Redirect
oc get route webapp should show TLS termination edge.
Curl HTTPS without a trusted CA

Expect a TLS verify error. Encryption is not the same as trust.

ROUTE=$(oc get route webapp -n 101-12-tls -o jsonpath='{.spec.host}')
curl -sS "https://${ROUTE}" | head -3 || true
curl should fail to verify the certificate.
Curl HTTPS with the lab CA

Pass --cacert so curl trusts 101-12-lab-ca. You should get the httpd page.

ROUTE=$(oc get route webapp -n 101-12-tls -o jsonpath='{.spec.host}')
curl -sS --cacert /tmp/101-12-certs/ca.crt "https://${ROUTE}" | head -5
HTML from the app. curl -k also loads the page, but it skips identity checks — a man-in-the-middle with any certificate would succeed.
Prove HTTP redirects to HTTPS

Insecure policy Redirect should send http:// to https://.

ROUTE=$(oc get route webapp -n 101-12-tls -o jsonpath='{.spec.host}')
curl -sI "http://${ROUTE}" | grep -iE 'HTTP/|location'
Look for a 302/301 and a Location: https:// header.
Show the certificate issuer on the wire

The issuer should be your lab CA, not the public ACME ingress CA.

ROUTE=$(oc get route webapp -n 101-12-tls -o jsonpath='{.spec.host}')
echo | openssl s_client -connect "${ROUTE}:443" -servername "${ROUTE}" 2>/dev/null \
  | openssl x509 -noout -subject -issuer -ext subjectAltName
Issuer 101-12-lab-ca. SAN matches the Route hostname.

Signed certificates vs self-signed

Encryption and trust are not the same thing.

Kind Who signs it What clients do

Self-signed

The certificate signs itself (issuer equals subject)

curl and browsers fail unless you pass -k or import that cert. Anyone can mint another self-signed cert for the same name.

Private CA (this lab)

Your 101-12-lab-ca

Clients that have the CA (--cacert or an org trust store) verify the leaf. Everyone else still sees an untrusted warning.

Public CA (this cluster’s ingress cert)

An ACME CA via acme-bifrost-production-ddns

Browsers already trust it. No -k. This is what production internet-facing Routes should use.

The cluster ingress certificate in openshift-ingress is the public-CA example. Your webapp certificate is the private-CA example. Both are "signed"; only the first is trusted by default on the public internet.

Certificate rotation

Certificates expire. If nobody renews them, TLS becomes an outage. Three patterns you will see on OpenShift:

Tool What it rotates When to use it

cert-manager (this lab)

Secrets from Certificate CRs, before expiry

Route and ingress certificates, including ACME. This cluster already renews cert-manager-ingress-cert.

HashiCorp Vault PKI

Short-lived certificates from a secrets engine, with TTL and revocation

App or service certificates whose source of truth should stay off the cluster. Hands-on secrets wiring is 201-05.

Zero Trust Workload Identity Manager (ZTWIM)

SPIFFE SVIDs bound to namespace and ServiceAccount; spiffe-helper renews them in the pod

East-west mTLS and workload identity, not the public Route. Hands-on is 301-08.

cert-manager solves "the Route cert expired at 2 a.m." Vault PKI solves "I need short-lived certs from a central PKI." ZTWIM solves "this pod must prove who it is to another service," not "the internet must trust my hostname."

Service-to-service mTLS with cert-manager is 201-08 (that lab also points a Certificate at this cluster’s ACME ClusterIssuer so a Route hostname is trusted without -k).

Debrief

You started from HTTP, issued a certificate with cert-manager, and accessed the same app on HTTPS with redirect. You also saw a publicly signed ingress certificate next to a lab CA that only curl --cacert trusts.

What breaks without this:

  • HTTP Routes → credential sniffing and cookie theft on the wire

  • No redirect → clients keep using insecure URLs

  • Self-signed plus curl -k as the permanent pattern → encryption without identity

  • No rotation → expiry outages and stale keys that never get replaced

HTTPS with redirect on Routes, cert-manager (or ACME) for issuance, a CA that matches your trust model, and a rotation path (cert-manager, Vault PKI, or ZTWIM depending on north-south vs east-west).
edge TLS is fine when the cluster internal path is trusted; re-encrypt or passthrough when you need end-to-end or app-owned certs. Do not use -k in production.
giphy

Cleanup

Before moving to the next module, run the lab cleanup script to reset transient resources from this module. It deletes project 101-12-tls only. ClusterIssuers and the ingress certificate stay in place.

cd ~/ocp5-rhacs-showroom
bash setup/lab-cleanup.sh --module 101-12