A few months ago I read Ludovico Caldara’s article on TLS authentication for Data Guard redo transport and thought, “I really need to try this.”

So I bookmarked it. Which, as we all know, is practically the same as building a lab. Right?

Eventually I found the time. There was one part of his example I wanted to approach differently: the certificates. Ludovico creates a lab CA and signs everything himself. That makes perfect sense when you want a reproducible demo, but it reminded me of something I’d encountered configuring Oracle GoldenGate at work.

I remembered a GoldenGate configuration I worked on for a customer. Their corporate PKI team sent me a .crt and a .p7b. As far as they were concerned, the certificate request was complete. Fair enough. But I still had to figure out what to do with those files on the Oracle side.

I asked Ludovico, Oracle Product Manager for Data Guard, whether OpenSSL-generated certificates could be used for the newer redo authentication feature. He confirmed they could. So this became my test: start with a CSR, pretend the corporate PKI team has returned the signed files, and get the whole thing working in Data Guard. No tutorial on running a Certificate Authority required.

A quick word about ORAPKI

This isn’t an OpenSSL versus orapki competition. You can generate a certificate request with either tool. The interesting difference is where the private key lives and what happens after the signed certificate comes back. orapki creates the key in an Oracle wallet; OpenSSL can keep it as a separate file, which means some extra wallet preparation later.

The lab

The database and Oracle Net tools in this test belong to Oracle AI Database 26ai, release 23.26.3.0.0, with OpenSSL 1.1.1k FIPS on the preparation host. The redo transport TLS authentication capability was introduced with release 23.26.2.0.0. The setup is a single-instance primary and physical standby managed by Data Guard Broker.

Primary:
DB_UNIQUE_NAME = DB1015_m8s_iad
Host = dbverse.sub08211110580.vcndbverselabs.oraclevcn.com
Standby:
DB_UNIQUE_NAME = DB1015_gdz_iad
Host = dbverse-stby.sub08211110580.vcndbverselabs.oraclevcn.com
TCPS listener port = 2484

I used a temporary lab CA to simulate the PKI service. In a real company, the issuing CA and certificate profile are normally managed elsewhere. The test root was trusted by both database hosts after the certificate-chain checks; the lab CA itself is not a production PKI recommendation.

1. Request the certificates

Two routes are possible. They lead to the same corporate PKI handoff, but you should stick to the route you chose for each identity. A signed certificate must match the private key from the original request.

ORAPKI: the key starts inside the wallet

With orapki, you create the wallet, generate the request and export the CSR. The signed certificate then goes back into that same wallet. That’s the documented Oracle workflow. For this experiment, though, I wanted to see what happened when I started with OpenSSL instead.

# Example on the primary, with a new wallet directory
PRIMARY_HOST="dbverse.sub08211110580.vcndbverselabs.oraclevcn.com"
ORA_WALLET="$HOME/dg-tls-primary/orapki-wallet"
orapki wallet create -wallet "$ORA_WALLET"
orapki wallet add -wallet "$ORA_WALLET" \
-dn "CN=$PRIMARY_HOST" -keysize 2048
orapki wallet export -wallet "$ORA_WALLET" \
-dn "CN=$PRIMARY_HOST" \
-request "$HOME/dg-tls-primary/primary-orapki.csr"

OpenSSL: the route I used

Here I generated a private key and CSR on the database host. I kept the subject short, using only the CN, in line with Ludovico’s example. I supplied the DNS SAN and intended TLS usages to the simulated issuer separately.

PRIMARY_HOST="dbverse.sub08211110580.vcndbverselabs.oraclevcn.com"
WORK="$HOME/dg-tls-primary"
umask 077
mkdir -p "$WORK"
cd "$WORK"
# Run only for a new request; do not overwrite an existing key.
if [ -e primary.key ] || [ -e primary.csr ]; then
echo "Existing key/CSR found. Inspect and reuse; no files overwritten."
else
openssl genpkey -algorithm RSA \
-pkeyopt rsa_keygen_bits:3072 -out primary.key
openssl req -new -sha256 -key primary.key \
-out primary.csr -subj "/CN=${PRIMARY_HOST}"
fi
# If only one of the two files exists, inspection will fail safely.
openssl req -in primary.csr -noout -subject -verify
OpenSSL-generated CSR for the primary database in the lab

For the standby I repeated the process with a different key and hostname. The CA does not need the database private key to sign the CSR; it needs the request and whatever additional identity/profile information its policy requires.

2. The PKI team replies. Now what?

Now imagine the PKI ticket is closed and these files appear in your received directory. It’s pretty close to what I was handed during that GoldenGate work:

dg-tls-primary/
primary.key # Already on the database host
primary.csr # Sent to PKI
received/
primary.crt # Signed database certificate
primary-chain.p7b # PKCS#7 certificate bundle
ca.crt # Trusted test CA certificate (lab case)

A .crt or .cer is usually a public certificate. A .p7b is a collection of certificates, often the issuing chain, not a private-key container. It may contain the leaf, intermediates and/or root; don’t assume the order or contents from the filename.

First I inspected the issued certificate and unpacked the PKCS#7 bundle using OpenSSL. The example P7B was DER encoded; a PEM-encoded bundle needs -inform PEM instead.

cd "$HOME/dg-tls-primary"
mkdir -p received/extracted
openssl x509 -in received/primary.crt \
-noout -subject -issuer -dates -ext subjectAltName
openssl pkcs7 -inform DER \
-in received/primary-chain.p7b \
-print_certs -out received/returned-certs.pem
awk '
/-----BEGIN CERTIFICATE-----/ {
n++
file=sprintf("received/extracted/cert-%02d.pem",n)
}
file != "" { print > file }
/-----END CERTIFICATE-----/ { close(file); file="" }
' received/returned-certs.pem
for cert in received/extracted/*.pem; do
openssl x509 -in "$cert" -noout -subject -issuer -dates
done
Inspecting the certificates extracted from the PKI-returned P7B

For this lab the CA signed the database certificate directly, so there were no intermediates. Once I’d verified the lab CA certificate, preparing the PEM chain was simple:

# Lab only: a verified, one-level test CA
cp received/ca.crt received/ca-chain.pem
openssl verify -CAfile received/ca.crt \
-verify_hostname "dbverse.sub08211110580.vcndbverselabs.oraclevcn.com" \
received/primary.crt

With a real corporate PKI, get the root from an approved source and assemble the correct intermediate chain. The files sent back by the PKI service are not automatically your trust policy. I also checked that the public key in the issued certificate matched the private key used for my CSR:

openssl pkey -in primary.key -pubout -outform DER \
| openssl dgst -sha256
openssl x509 -in received/primary.crt -pubkey -noout \
| openssl pkey -pubin -outform DER \
| openssl dgst -sha256

Both fingerprints should be identical. If they aren’t, wallet creation is not going to fix the mismatch.

3. From signed files to an Oracle wallet

If ORAPKI generated the CSR

The key is already inside the original Oracle wallet. Import the approved CA chain (intermediate certificates where applicable), then the signed user certificate. There is no reason to generate another key.

ORA_WALLET="$HOME/dg-tls-primary/orapki-wallet"
cd "$HOME/dg-tls-primary"
# One-level lab CA; import intermediates separately if present.
orapki wallet add -wallet "$ORA_WALLET" \
-trusted_cert -cert received/ca.crt
orapki wallet add -wallet "$ORA_WALLET" \
-user_cert -cert received/primary.crt
orapki wallet create -wallet "$ORA_WALLET" -auto_login
orapki wallet display -wallet "$ORA_WALLET" -complete

If OpenSSL generated the CSR

In my lab, I started with primary.key, the signed primary.crt, and the CA chain. OpenSSL combined them into a password-protected PKCS#12 file:

cd "$HOME/dg-tls-primary"
STAGING="$HOME/dg-tls-primary/openssl-p12"
umask 077
mkdir -p "$STAGING"
openssl pkcs12 -export \
-inkey primary.key \
-in received/primary.crt \
-certfile received/ca-chain.pem \
-name dg-primary \
-out "$STAGING/ewallet.p12"
openssl pkcs12 -in "$STAGING/ewallet.p12" -info -noout

So far, so good. OpenSSL could read the file, and orapki wallet display recognized both the database certificate and the trusted CA. I enabled auto-login and got the reassuring message that the operation had completed successfully.

OpenSSL PKCS12 wallet export and inspection during the Oracle Data Guard TLS lab

Unfortunately, the listener had other opinions.

4. The wallet that looked fine, until it didn’t

The original wallet still prompted for a password when I ran orapki wallet display, even after auto-login creation. Then the listener failed as it tried to initialize TCPS:

ORA-29113: Cannot access attributes from a PKCS #12 key bag.
TNS-00566: SSL protocol adapter failed to open wallet
Original wallet inspection still prompting for a password after auto-login creation

At first I suspected auto-login. OpenSSL could read the PKCS#12 and decrypt the private key, while ORAPKI could list the database certificate and the trusted CA. So the contents were there, but something still wasn’t behaving as expected. I couldn’t tell whether the problem was auto-login itself or how Oracle was processing the original wallet and its key bag.

Instead of generating another certificate, I created a new Oracle wallet, imported the same OpenSSL PKCS#12, and enabled auto-login again:

SOURCE="$HOME/dg-tls-primary/openssl-p12/ewallet.p12"
NATIVE="$HOME/dg-tls-primary/wallet-native-test"
# Use a fresh directory. The commands prompt for passwords.
orapki wallet create -wallet "$NATIVE"
orapki wallet import_pkcs12 \
-wallet "$NATIVE" \
-pkcs12file "$SOURCE"
orapki wallet create -wallet "$NATIVE" -auto_login
orapki wallet display -wallet "$NATIVE" -complete
Certificate inventory after importing the OpenSSL PKCS12 into an Oracle-created wallet
Auto-login and passwordless ORAPKI wallet display after import

This time, the wallet display didn’t ask for a password. And after deploying the resulting wallet into the configured listener wallet location, the listener started on TCPS port 2484.

Listening on: (DESCRIPTION=(ADDRESS=(PROTOCOL=tcp)(...)(PORT=1521)))
Listening on: (DESCRIPTION=(ADDRESS=(PROTOCOL=tcps)(...)(PORT=2484)))
The command completed successfully

Same certificate. Same private key. Different wallet preparation. That is what resolved the listener startup failure in my 23.26.3.0.0 lab. I haven’t isolated the exact internal difference, so I’m not claiming that OpenSSL-created PKCS#12 files are generally incompatible or that auto-login was definitively the root cause.

One operational detail: in my OCI database environment, the listener wallet was under /opt/oracle/dcs/commonstore/tcps_wallet/. I backed up the existing primary wallet before replacing it; there wasn’t an equivalent wallet to back up on the standby. That path belongs to the DCS-managed area in this deployment, so don’t assume it’s a universal TCPS wallet location or copy files into a managed directory without checking its lifecycle and support implications.

5. Getting Data Guard onto certificates

A running TCPS listener was only the first milestone. We still needed both database hosts to trust each other’s certificates and Oracle to associate each sending database with an external identity.

After the wallet issue was sorted out, I configured TCPS on both hosts: a listener endpoint on port 2484, certificate-aware client/server settings, and a TNS alias for each database. Ludovico covers the overall Data Guard procedure in his article, so I’ll concentrate here on the settings I actually used and what I checked afterward.

Here’s the primary configuration. The standby uses its own hostname, service and wallet, but follows the same pattern.

# listener.ora Network Configuration File: /u01/app/oracle/product/23.0.0/dbhome_1/network/admin/listener.ora
# Generated by Oracle configuration tools.
#listener.oraNetworkConfigurationFile:/u01/app/oracle/product/23.0.0/dbhome_1/network/admin/listener.ora
#GeneratedbyOracleconfigurationtools.
SID_LIST_LISTENER =
(SID_LIST =
(SID_DESC =
(GLOBAL_DBNAME = DB1015_m8s_iad.sub08211110580.vcndbverselabs.oraclevcn.com)
(ORACLE_HOME = /u01/app/oracle/product/23.0.0/dbhome_1)
(SID_NAME = DB1015)
)
)
WALLET_LOCATION =
(SOURCE =
(METHOD = FILE)
(METHOD_DATA =
(DIRECTORY = /opt/oracle/dcs/commonstore/tcps_wallet)
)
)
LISTENER =
(DESCRIPTION_LIST =
(DESCRIPTION =
(ADDRESS = (PROTOCOL = TCP)(HOST = dbverse.sub08211110580.vcndbverselabs.oraclevcn.com)(PORT = 1521))
(ADDRESS = (PROTOCOL = IPC)(KEY = EXTPROC1521))
(ADDRESS = (PROTOCOL = TCPS)(HOST = dbverse.sub08211110580.vcndbverselabs.oraclevcn.com)(PORT = 2484))
)
)

These are the lines I added or adjusted in sqlnet.ora for the test:

Two notes about this exact tested configuration: SSL_CLIENT_AUTHENTICATION=TRUE enables mutual TLS, although Oracle 26ai prefers the newer TLS_CLIENT_AUTHENTICATION parameter name. I kept the old spelling here because that’s what I tested; I haven’t repeated this lab using the newer spelling. I also retained SQLNET.FALLBACK_AUTHENTICATION=TRUE, as in Ludovico’s example. However, Oracle’s Net Services documentation describes that parameter as Kerberos authentication fallback. I haven’t established that it is needed for TLS redo authentication, and I’d like to clarify that separately rather than present it as a minimum requirement.


WALLET_LOCATION=(SOURCE=(METHOD=FILE)(METHOD_DATA=(DIRECTORY=/opt/oracle/dcs/commonstore/tcps_wallet)))
SSL_CLIENT_AUTHENTICATION=TRUE
SQLNET.AUTHENTICATION_SERVICES = (BEQ,TCPS)
SQLNET.FALLBACK_AUTHENTICATION=TRUE

I also added separate TCPS aliases to tnsnames.ora:

DB1015_M8S_IAD_TCPS=(DESCRIPTION=(SDU=65535)(SEND_BUF_SIZE=10485760)(RECV_BUF_SIZE=10485760)(ADDRESS=(PROTOCOL=TCPS)(HOST=dbverse.sub08211110580.vcndbverselabs.oraclevcn.com)(PORT=2484))(CONNECT_DATA=(SERVER=DEDICATED)(SERVICE_NAME=DB1015_m8s_iad.sub08211110580.vcndbverselabs.oraclevcn.com)))
DB1015_GDZ_IAD_TCPS=(DESCRIPTION=(SDU=65535)(SEND_BUF_SIZE=10485760)(RECV_BUF_SIZE=10485760)(ADDRESS=(PROTOCOL=TCPS)(HOST=dbverse-stby.sub08211110580.vcndbverselabs.oraclevcn.com)(PORT=2484))(CONNECT_DATA=(SERVER=DEDICATED)(SERVICE_NAME=DB1015_gdz_iad.sub08211110580.vcndbverselabs.oraclevcn.com)))

Both hosts need entries that resolve correctly in both directions. I found that out the annoying way when local TCPS worked but connections between the hosts initially failed certificate validation. In that test I’d created separate lab CAs, so fixing the trust setup was part of getting the two sides to talk.

I restarted the listeners and checked that the TCPS endpoints appeared in lsnrctl status.

lsnrctl stop
lsnrctl start
lsnrctl status

The database instances also needed to pick up the updated wallet and Oracle Net settings in this setup.

I followed Ludovico’s procedure and restarted both instances after the wallet and Oracle Net changes. Oracle’s 26ai TLS setup instructions also describe restarting server and listener processes to pick up certain configuration changes. That doesn’t mean every certificate renewal always needs a database restart. This is a service interruption: I used STARTUP FORCE in the lab, but that performs an abort-style shutdown before starting the instance. For a managed production environment, plan a controlled restart using the appropriate database lifecycle tools instead.

sqlplus / as sysdba <<EOF
startup force
EOF

After restarting the relevant services in the lab, I checked the TNS aliases in both directions and used an actual SQL connection over TCPS. tnsping is a useful first check, but it doesn’t prove the database authenticated the client using its certificate.

sqlplus /@DB1015_M8S_IAD_TCPS as sysoper
SELECT SYS_CONTEXT('USERENV','NETWORK_PROTOCOL') AS protocol,
SYS_CONTEXT('USERENV','AUTHENTICATION_METHOD') AS auth_method,
SYS_CONTEXT('USERENV','AUTHENTICATED_IDENTITY') AS identity
FROM dual;
SQLPlus output showing a TCPS connection and certificate-based external authentication

Before changing Broker, I checked the existing DGConnectIdentifier properties and validated the new TCPS aliases from both database hosts. Only after those connection tests succeeded did I switch the properties to the TCPS identifiers.

DGMGRL> show configuration
Configuration - db1015_dgconf
Protection Mode: MaxPerformance
Members:
DB1015_m8s_iad - Primary database
DB1015_gdz_iad - Physical standby database
Fast-Start Failover: Disabled
Configuration Status:
SUCCESS (status updated 36 seconds ago)
DGMGRL> show database DB1015_m8s_iad DGConnectIdentifier
DGConnectIdentifier = 'DB1015_m8s_iad'
DGMGRL> show database DB1015_gdz_iad DGConnectIdentifier
DGConnectIdentifier = 'DB1015_gdz_iad'
DGMGRL> validate DGConnectIdentifier DB1015_m8s_iad_tcps
At instance 'DB1015' of member 'DB1015_m8s_iad'
'db1015_m8s_iad_tcps' translates to:
(DESCRIPTION=(SDU=65535)(SEND_BUF_SIZE=10485760)(RECV_BUF_SIZE=10485760)(ADDRESS=(PROTOCOL=TCPS)(HOST=dbverse.sub08211110580.vcndbverselabs.oraclevcn.com)(PORT=2484))(CONNECT_DATA=(SERVER=DEDICATED)(SERVICE_NAME=DB1015_m8s_iad.sub08211110580.vcndbverselabs.oraclevcn.com)))
Environment Variables:
TNS_ADMIN:
ORACLE_HOME: /u01/app/oracle/product/23.0.0/dbhome_1
ORACLE_BASE:
Initialization Parameters:
LOCAL_LISTENER:
Connection Test Result:
Connected to instance 'DB1015' at member 'DB1015_m8s_iad'
At instance 'DB1015' of member 'DB1015_gdz_iad'
'db1015_m8s_iad_tcps' translates to:
(DESCRIPTION=(SDU=65535)(SEND_BUF_SIZE=10485760)(RECV_BUF_SIZE=10485760)(ADDRESS=(PROTOCOL=TCPS)(HOST=dbverse.sub08211110580.vcndbverselabs.oraclevcn.com)(PORT=2484))(CONNECT_DATA=(SERVER=DEDICATED)(SERVICE_NAME=DB1015_m8s_iad.sub08211110580.vcndbverselabs.oraclevcn.com)))
Environment Variables:
TNS_ADMIN:
ORACLE_HOME: /u01/app/oracle/product/23.0.0/dbhome_1
ORACLE_BASE:
Initialization Parameters:
LOCAL_LISTENER: LISTENER_DB1015
Connection Test Result:
Connected to instance 'DB1015' at member 'DB1015_m8s_iad'
DGMGRL> validate DGConnectIdentifier DB1015_gdz_iad_tcps
At instance 'DB1015' of member 'DB1015_m8s_iad'
'db1015_gdz_iad_tcps' translates to:
(DESCRIPTION=(SDU=65535)(SEND_BUF_SIZE=10485760)(RECV_BUF_SIZE=10485760)(ADDRESS=(PROTOCOL=TCPS)(HOST=dbverse-stby.sub08211110580.vcndbverselabs.oraclevcn.com)(PORT=2484))(CONNECT_DATA=(SERVER=DEDICATED)(SERVICE_NAME=DB1015_gdz_iad.sub08211110580.vcndbverselabs.oraclevcn.com)))
Environment Variables:
TNS_ADMIN:
ORACLE_HOME: /u01/app/oracle/product/23.0.0/dbhome_1
ORACLE_BASE:
Initialization Parameters:
LOCAL_LISTENER:
Connection Test Result:
Connected to instance 'DB1015' at member 'DB1015_gdz_iad'
At instance 'DB1015' of member 'DB1015_gdz_iad'
'db1015_gdz_iad_tcps' translates to:
(DESCRIPTION=(SDU=65535)(SEND_BUF_SIZE=10485760)(RECV_BUF_SIZE=10485760)(ADDRESS=(PROTOCOL=TCPS)(HOST=dbverse-stby.sub08211110580.vcndbverselabs.oraclevcn.com)(PORT=2484))(CONNECT_DATA=(SERVER=DEDICATED)(SERVICE_NAME=DB1015_gdz_iad.sub08211110580.vcndbverselabs.oraclevcn.com)))
Environment Variables:
TNS_ADMIN:
ORACLE_HOME: /u01/app/oracle/product/23.0.0/dbhome_1
ORACLE_BASE:
Initialization Parameters:
LOCAL_LISTENER: LISTENER_DB1015
Connection Test Result:
Connected to instance 'DB1015' at member 'DB1015_gdz_iad'
DGMGRL> edit database DB1015_m8s_iad set property DGConnectIdentifier = DB1015_m8s_iad_tcps;
Property "dgconnectidentifier" updated for member "db1015_m8s_iad".
DGMGRL> show database DB1015_m8s_iad DGConnectIdentifier
DGConnectIdentifier = 'db1015_m8s_iad_tcps'
DGMGRL> edit database DB1015_gdz_iad set property DGConnectIdentifier = DB1015_gdz_iad_tcps;
Property "dgconnectidentifier" updated for member "db1015_gdz_iad".
DGMGRL> show database DB1015_gdz_iad DGConnectIdentifier
DGConnectIdentifier = 'db1015_gdz_iad_tcps'
DGMGRL> validate network configuration for all;
Validating at "DB1015_m8s_iad"
Instance "DB1015": successfully connected to instance "DB1015" on member "DB1015_gdz_iad"
Validating at "DB1015_gdz_iad"
Instance "DB1015": successfully connected to instance "DB1015" on member "DB1015_m8s_iad"
Oracle Clusterware is not configured on database "DB1015_m8s_iad".
Connecting to database "DB1015_m8s_iad" using static connect identifier "DB1015_M8S_IAD_DGMGRL" ...
Succeeded.
The static connect identifier allows for a connection to database "DB1015_m8s_iad".
Oracle Clusterware is not configured on database "DB1015_gdz_iad".
Connecting to database "DB1015_gdz_iad" using static connect identifier "DB1015_GDZ_IAD_DGMGRL" ...
Succeeded.
The static connect identifier allows for a connection to database "DB1015_gdz_iad".
DGMGRL> show configuration;
Configuration - db1015_dgconf
Protection Mode: MaxPerformance
Members:
DB1015_m8s_iad - Primary database
DB1015_gdz_iad - Physical standby database
Fast-Start Failover: Disabled
Configuration Status:
SUCCESS (status updated 22 seconds ago)

Mapping the certificate identities

Oracle AI Database 26ai requires password-file format 12.2 for externally authenticated administrative users. Mine was in format 12, so I converted it before creating the certificate-mapped users. One warning about the command I actually ran: using the same path for FILE and INPUT_FILE with FORCE=Y replaces the existing password file. I didn’t make a separate backup first in this lab. Not a step I’d recommend repeating! For a real environment, back up the file or generate a new output file, validate it, and plan the replacement. ASM and managed database password files need their own supported procedure.

# Safer example for an OS-file-backed password file only.
# This creates a new file; it does NOT replace the active one.
PWFILE="$ORACLE_HOME/dbs/orapw$ORACLE_SID"
PWFILE_NEW="${PWFILE}.12_2"
# Keep a restorable copy before any migration.
cp -p "$PWFILE" "${PWFILE}.backup.$(date +%Y%m%d%H%M%S)"
orapwd FILE="$PWFILE_NEW" INPUT_FILE="$PWFILE" FORMAT=12.2
# Arrange a controlled replacement appropriate for your platform.
# Recheck after activation, not merely after generating PWFILE_NEW:
# SQL> SELECT file_name, format FROM v$passwordfile_info;

In my test the active password file subsequently reported format 12.2. The example above deliberately creates a separate output instead of repeating my in-place FORCE=Y replacement. Check administrative access and the actual password-file location before and after any change.

I used one certificate identity for each database. The external name must match the certificate’s complete subject DN; in this lab the subject contains only a CN. The users and grants were created for the participating databases and verified in the password-file view.

-- In the CDB root, with appropriate administrative privileges:
CREATE USER C##REDO_DB1015_M8S_IAD
IDENTIFIED EXTERNALLY AS
'CN=dbverse.sub08211110580.vcndbverselabs.oraclevcn.com'
CONTAINER=ALL;
GRANT SYSOPER TO C##REDO_DB1015_M8S_IAD CONTAINER=ALL;
CREATE USER C##REDO_DB1015_GDZ_IAD
IDENTIFIED EXTERNALLY AS
'CN=dbverse-stby.sub08211110580.vcndbverselabs.oraclevcn.com'
CONTAINER=ALL;
GRANT SYSOPER TO C##REDO_DB1015_GDZ_IAD CONTAINER=ALL;

The sender needs to present its own identity, not whichever identity happens to belong to the current primary role. That matters when roles change.

-- On DB1015_m8s_iad:
ALTER SYSTEM SET redo_transport_user=C##REDO_DB1015_M8S_IAD;
-- On DB1015_gdz_iad:
ALTER SYSTEM SET redo_transport_user=C##REDO_DB1015_GDZ_IAD;

For the Broker-managed configuration, I validated the TCPS connect descriptors before updating the relevant DGConnectIdentifier settings, then checked that Broker and redo transport remained healthy. The exact service names and listener registration depend on the environment; I used my existing database services rather than blindly copying a template.

Evidence from the receiving side

I also inspected the RFS processes on the receiving database, which is a more relevant check for redo authentication than another TCP or TCPS ping:

SELECT dg.name AS dg_process,
dg.role,
s.username,
s.external_name,
s.machine
FROM v$dataguard_process dg
JOIN v$process p ON p.spid = dg.pid
JOIN v$session s ON s.paddr = p.addr
WHERE LOWER(dg.name) LIKE '%rfs%';

In my RFS snapshot, RFS ping showed the sending database’s certificate subject in EXTERNAL_NAME: CN=dbverse.sub08211110580.vcndbverselabs.oraclevcn.com

6. One more test: switchover

Everything was looking good, but I wanted to make sure it wasn’t a setup that worked only while the original primary kept its role. So I validated both databases with Broker and switched over to DB1015_gdz_iad. No certificate recreation, no wallet replacement.

DGMGRL> VALIDATE DATABASE DB1015_m8s_iad;
DGMGRL> VALIDATE DATABASE DB1015_gdz_iad;
DGMGRL> SWITCHOVER TO DB1015_gdz_iad;

SHOW CONFIGURATION returned SUCCESS, with DB1015_gdz_iad as the new primary and DB1015_m8s_iad as physical standby.

DGMGRL> switchover to DB1015_gdz_iad;
2026-10-28T22:47:43.149+00:00
Performing switchover NOW, please wait...
2026-10-28T22:47:43.174+00:00
Operation requires a connection to database "DB1015_gdz_iad"
Connecting ...
Connected to "DB1015_gdz_iad"
Connected as SYSDBA.
2026-10-28T22:47:43.268+00:00
Continuing with the switchover...
2026-10-28T22:47:50.093+00:00
New primary database "DB1015_gdz_iad" is opening...
2026-10-28T22:47:50.093+00:00
Operation requires start up of instance "DB1015" on database "DB1015_m8s_iad"
Starting instance "DB1015"...
Connected to an idle instance.
ORACLE instance started.
Connected to "DB1015_m8s_iad"
Database mounted.
Database opened.
2026-10-28T22:48:08.308+00:00
Switchover succeeded, new primary is "db1015_gdz_iad"
2026-10-28T22:48:08.320+00:00
Switchover processing complete, broker ready.
DGMGRL> show configuration
Configuration - db1015_dgconf
Protection Mode: MaxPerformance
Members:
DB1015_gdz_iad - Primary database
DB1015_m8s_iad - Physical standby database
Fast-Start Failover: Disabled
Configuration Status:
SUCCESS (status updated 26 seconds ago)

Broker completed the switchover, opened DB1015_gdz_iad as the new primary, and subsequently showed SUCCESS with the roles reversed. No certificate recreation, no wallet replacement. But I wanted one more piece of evidence from the receiving side after the roles changed.

One last check: RFS after switchover

After the switchover, DB1015_gdz_iad became the new primary, and DB1015_m8s_iad became the physical standby. Broker looked good, but there’s one more useful check: the certificate identity RFS sees in the reverse direction. On the new standby, run:

SET LINESIZE 250
COLUMN USERNAME FORMAT A25
COLUMN EXTERNAL_NAME FORMAT A85
SELECT dg.name AS dg_process,
dg.role,
dg.action,
s.username,
s.external_name,
s.machine
FROM v$dataguard_process dg
JOIN v$process p
ON p.spid = dg.pid
JOIN v$session s
ON s.paddr = p.addr
WHERE LOWER(dg.name) LIKE '%rfs%';
RFS process query output on the new physical standby after Data Guard switchover
RFS query after switchover. Open the image to inspect the original full-resolution output.

The field to inspect is EXTERNAL_NAME. On the new standby, compare it with the certificate subject of the new primary, CN=dbverse-stby.sub08211110580.vcndbverselabs.oraclevcn.com. This provides another identity check in the reverse direction, alongside the TCPS SQL connection and Broker evidence. The original screenshot is linked above so the output can be inspected at full resolution.

Final thoughts

So, did the OpenSSL experiment work? Yes, although not quite as I’d expected. OpenSSL generated the CSR and PKCS#12 without trouble, but my first wallet didn’t behave as expected when I enabled auto-login, and the listener failed with ORA-29113. Importing that same PKCS#12 into an Oracle-created wallet and enabling auto-login there resolved it. I still don’t know which internal difference mattered, so I can’t call it a general OpenSSL limitation. For Oracle wallet management, I’d reach for orapki next time.

But replacing orapki wasn’t really the point. I wanted to reproduce a familiar situation: the corporate PKI team signs your request, sends back a .crt and a .p7b, and you’re left to make them work with Oracle. In this lab, we got from those files to certificate-authenticated SQL connections, a working TCPS setup, and a successful Data Guard switchover.

Not bad for something that spent months in my bookmarks.

References

Oracle AI Database 26ai, sqlnet.ora Parameters (including authentication fallback and the newer TLS parameter names)

Ludovico Caldara, Configure Oracle Data Guard Redo Transport with TLS Authentication

Oracle AI Database 26ai, Redo Transport Services

Oracle AI Database 26ai, Managing Wallets and Certificates

Oracle AI Database 26ai, Configuring TLS Encryption

Oracle ORA-29113 Error Reference

Leave a Reply

Trending