MongoDB Istio Split-Horizon Replica Set
Configuring Split-Horizon Ingress with Istio and MongoDB in Kubernetes
Prerequisites and Assumptions
- MongoDB Enterprise Operator and Ops Manager infrastructure is already deployed.
- Istio is not injecting mesh network sidecars on the deployment namespace. We will need to ensure TLS connections terminate at the
mongod
directly. - You have an accessible Load Balancer and created DNS records for your external horizon addresses to point to this public address.
- You have cert-manager deployed and an Issuer (Certificate Authority) configured
TLS Certificates from cert-manager
The following certificate request includes the Subject Alternative Names list of both the external connection addresses as well as the internal pods’ fully qualified domain name (FQDN). The certificate usages including both server auth
and client auth
is required for MongoDB, regardless of split-horizon situations. The spec.secretName
includes an important “prefix” followed by the MongoDB metadata.name
and a cert
suffix, e.g. mdb-{name}-cert
. This is required for the Operator to hook onto the kubernetes secret that cert-manager generates. The Operator is then expected to create a new secret (type opaque) that takes the name format of mdb-{name}-cert-pem
(the operator concatenated the tls secret’s key and cert into a single data field). MongoDB configuration typically requires a single PEM file that contains both the private key and certificate. The Operator mounts this secret to the MongoDB statefulset’s pods.
---
apiVersion: cert-manager.io/v1
kind: Certificate
metadata:
name: my-scram-enabled-replica-set-cert
namespace: mongodb
spec:
# Secret names are always required.
secretName: mdb-my-scram-enabled-replica-set-cert
duration: 2160h # 90d
renewBefore: 360h # 15d
subject:
organizations:
- jalder
# The use of the common name field has been deprecated since 2000 and is
# discouraged from being used.
commonName: my-tls-enabled-rs-0
isCA: false
privateKey:
algorithm: RSA
encoding: PKCS1
size: 2048
usages:
- server auth
- client auth
# At least one of a DNS Name, URI, or IP address is required.
dnsNames:
- my-scram-enabled-replica-set-0.my-scram-enabled-replica-set-svc.mongodb.svc.cluster.local
- my-scram-enabled-replica-set-1.my-scram-enabled-replica-set-svc.mongodb.svc.cluster.local
- my-scram-enabled-replica-set-2.my-scram-enabled-replica-set-svc.mongodb.svc.cluster.local
- mdb0.k3s.jalder.tech
- mdb1.k3s.jalder.tech
- mdb2.k3s.jalder.tech
# Issuer references are always required.
issuerRef:
name: ca-issuer
kind: Issuer
group: cert-manager.io
MongoDB manifest
The following MongoDB manifest enables both TLS and SCRAM-SHA-256 authentication mechanism. It’s presumed your Ops Manager project connection details are in a configmap named my-project
and your Ops Manager API keys are in kubernetes secret my-credentials
. Note the certsSecretPrefix
is set to mdb
which was mentioned as a requirement in the previous step. This informs the Operator which TLS secret maps to this MongoDB deployment. The cert-manager Issuer’s Certificate Authority chain is saved in a k8s configmap named custom-ca
with a data field named ca-pem
(e.g. kubectl create cm custom-ca --from-file=ca-pem
). The replicaSetHorizons
has been populated with an ordered array of the external addresses or hostnames that you will reach the service at. Note that IP Addresses are not allowed in MongoDB split-horizon as the TLS SNI RFC states that SNI does not accommodate IP Addresses (some libraries will automatically look up the rDNS of an IP when negotiating TLS anyways).
---
apiVersion: mongodb.com/v1
kind: MongoDB
metadata:
name: my-scram-enabled-replica-set
namespace: mongodb
spec:
credentials: my-credentials
members: 3
opsManager:
configMapRef:
name: my-project
security:
certsSecretPrefix: mdb
tls:
enabled: true
ca: custom-ca
authentication:
agents:
mode: SCRAM
enabled: true
modes:
- SCRAM
type: ReplicaSet
version: 4.4.12-ent
connectivity:
replicaSetHorizons:
- "istio-gw": "mdb0.k3s.jalder.tech:27017"
- "istio-gw": "mdb1.k3s.jalder.tech:27017"
- "istio-gw": "mdb2.k3s.jalder.tech:27017"
MongoDB Pod Services
Three services are created, one for each pod. The selector targets each pod individually, this is important for the external MongoDB drivers as they need to contact each replica set member directly and individually in order to route queries correctly.
---
apiVersion: v1
kind: Service
metadata:
name: my-scram-enabled-replica-set-0
namespace: mongodb
spec:
selector:
statefulset.kubernetes.io/pod-name: my-scram-enabled-replica-set-0
ports:
- protocol: TCP
port: 27017
targetPort: 27017
---
apiVersion: v1
kind: Service
metadata:
name: my-scram-enabled-replica-set-1
namespace: mongodb
spec:
selector:
statefulset.kubernetes.io/pod-name: my-scram-enabled-replica-set-1
ports:
- protocol: TCP
port: 27017
targetPort: 27017
---
apiVersion: v1
kind: Service
metadata:
name: my-scram-enabled-replica-set-2
namespace: mongodb
spec:
selector:
statefulset.kubernetes.io/pod-name: my-scram-enabled-replica-set-2
ports:
- protocol: TCP
port: 27017
targetPort: 27017
MongoDB VirtualService Routing
An istio VirtualService is created to bind the internal pod services with the gateway service. Note the sniHosts
route individually to their respective replica set members. We’re catching all wildcard subdomains on the external address, adjust this as appropriate for your environment. The route destinations point to the pod service’s metadata.name. We will be routing port 27017 1:1 throughout this project, but you may change ports at the horizon if desired (update the replicaSetHorizons
with appropriate ports).
---
apiVersion: networking.istio.io/v1beta1
kind: VirtualService
metadata:
name: my-scram-enabled-replica-set
namespace: mongodb
spec:
hosts:
- "*.k3s.jalder.tech" # external DNS hostnames pointing at the LoadBalancer
gateways:
- mongodb/mdb-gateway # can omit the namespace if gateway is in same namespace as virtual service.
tls:
- match:
- port: 27017
sniHosts:
- mdb0.k3s.jalder.tech
route:
- destination:
host: my-scram-enabled-replica-set-0
- match:
- port: 27017
sniHosts:
- mdb1.k3s.jalder.tech
route:
- destination:
host: my-scram-enabled-replica-set-1
- match:
- port: 27017
sniHosts:
- mdb2.k3s.jalder.tech
route:
- destination:
host: my-scram-enabled-replica-set-2
Istio Gateway
This is a minimal Istio Gateway bound to the ingressgateway. Your plans may deviate from here, this example is meant to simplify the project, merge the appropriate servers entry into your existing Istio gateway if appropriate.
The important portions are the tls.mode
set to PASSTHROUGH
and the port.protocol
set to TLS
. MongoDB split-horizon functionality requires that the original TLS Client Hello message (where the desired SNI is requested), reaches the mongod
process directly, no early termination of TLS can be accommodated (aside from edge cases like relaying the original SNI).
---
apiVersion: networking.istio.io/v1beta1
kind: Gateway
metadata:
name: mdb-gateway
namespace: mongodb
spec:
selector:
istio: ingressgateway
servers:
- port:
number: 27017
name: mongo
protocol: TLS
tls:
mode: PASSTHROUGH
hosts:
- "*"
Connect and Validate Horizon
If everything went as planned, you should now be able to connect to the deployment at the external horizon addresses. MongoDB split-horizon configuration means that a client driver’s request for SDAM (server discovery and monitoring) queries returns the horizon address rather than the internal addresses. Connecting to the deployment and calling db.hello()
(formerly db.isMaster()
) from mongo shell will return internal hosts or horizon hosts when configured correctly. Note the external hostnames are returned as expected.
Connect:
mongo mongodb://mdb0.k3s.jalder.tech/?replicaSet=my-scram-enabled-replica-set --tls --tlsCAFile ca.crt
Confirm Horizons
MongoDB Enterprise my-scram-enabled-replica-set:PRIMARY> db.hello()
{
"topologyVersion" : {
"processId" : ObjectId("62571946dc7cc56286694ab1"),
"counter" : NumberLong(6)
},
"hosts" : [
"mdb0.k3s.jalder.tech:27017",
"mdb1.k3s.jalder.tech:27017",
"mdb2.k3s.jalder.tech:27017"
],
"setName" : "my-scram-enabled-replica-set",
"setVersion" : 35,
"isWritablePrimary" : true,
"secondary" : false,
"primary" : "mdb0.k3s.jalder.tech:27017",
"me" : "mdb0.k3s.jalder.tech:27017",
"electionId" : ObjectId("7fffffff000000000000000b"),
"lastWrite" : {
"opTime" : {
"ts" : Timestamp(1649893435, 1),
"t" : NumberLong(11)
},
"lastWriteDate" : ISODate("2022-04-13T23:43:55Z"),
"majorityOpTime" : {
"ts" : Timestamp(1649893435, 1),
"t" : NumberLong(11)
},
"majorityWriteDate" : ISODate("2022-04-13T23:43:55Z")
},
"maxBsonObjectSize" : 16777216,
"maxMessageSizeBytes" : 48000000,
"maxWriteBatchSize" : 100000,
"localTime" : ISODate("2022-04-13T23:43:56.222Z"),
"logicalSessionTimeoutMinutes" : 30,
"connectionId" : 5865,
"minWireVersion" : 0,
"maxWireVersion" : 9,
"readOnly" : false,
"ok" : 1,
"$clusterTime" : {
"clusterTime" : Timestamp(1649893435, 1),
"signature" : {
"hash" : BinData(0,"B5f/lRzd/KGVnSze5Y6stVRCd/E="),
"keyId" : NumberLong("7085396157957931012")
}
},
"operationTime" : Timestamp(1649893435, 1)
}