Using Client-to-Site VPN to privately connect to Secrets Manager
You can securely reach a private-only IBM Cloud® Secrets Manager over a IBM Cloud–native path from a client workstation (for example, a Mac using OpenVPN Connect or another OpenVPN-compatible client), by using Client-to-Site (C2S) VPN Gateway in VPC and a Virtual Private Endpoint (VPE).
When the VPN establishes a connection, it allows the source device to send traffic for the VPE IP through its tunnel. On the VPN server side, a translate (SNAT) route makes traffic to the VPE IP appear as if it originates from the VPN server's own private IP inside the VPC. This configuration is required because the VPE accepts traffic only from inside the VPC address space.
The VPE Security Group allows HTTPS (TCP 443) only from the VPN server's private IP to the VPE IP. This means that only clients coming through this VPN server can reach Secrets Manager. No other resources in the VPC or on the internet can communicate with the VPE.
In this example, domain name resolution uses /etc/hosts. For production-grade configuration, replace this with IBM Cloud DNS Services.
Before you begin
Before you set up the VPN connection, ensure that you have the following prerequisites in place.
Required instances and services
- A Secrets Manager instance for managing the Client-to-Site VPN certificates
- A private-only Secrets Manager instance that you want to access through the VPN
- An existing IBM Cloud VPC and subnet in your gateway region (GW_REGION)
Required software and tools
- macOS with OpenVPN Connect (or another OpenVPN-compatible client) installed
- IBM Cloud CLI and VPC plugin:
ibmcloud plugin install vpc-infrastructure jqcommand-line JSON processor
Disconnect all other VPN clients before testing this configuration to avoid routing conflicts.
Environment variables
export SM_REGION="us-south" # Secrets Manager region
export GW_REGION="us-south" # Region where your VPC, VPE, and VPN live
export RG_NAME="Default" # Resource Group
export SM_CRN="<secrets-manager-instance-crn>" # Secrets Manager instance CRN
export SM_HOST="<secrets-manager-instance-host>" # Secrets Manager instance host <instance_id.private.region.secrets-manager.appdomain.cloud>
export SM_PKI_ENDPOINT="<secrets-manager-pki-instance-url>" # Secrets Manager PKI instance endpoint URL <https://instance_id.region.secrets-manager.appdomain.cloud>
export VPC_ID="<existing-vpc-id>" # Existing VPC ID in GW_REGION
export SUBNET_ID="<existing-subnet-id>" # Existing subnet ID in GW_REGION
export VPE_NAME="test-vpe-gw" # Name for the VPE gateway
export SG_VPE_NAME="test-vpe-sg" # Security Group to attach to the VPE
export VPN_NAME="test-vpn" # Name for the Client-to-Site VPN
export SG_VPN_NAME="test-vpn-sg" # Security Group to attach to the VPN
export CLIENT_POOL="10.241.0.0/22" # VPN client IP pool
Log in to your IBM Cloud account
ibmcloud login -r "$GW_REGION" --sso
ibmcloud target -g "$RG_NAME"
Create the VPE Security Group
Create a dedicated Security Group for the VPE and capture its ID.
ibmcloud is security-group-create "$SG_VPE_NAME" "$VPC_ID"
export VPE_SG_ID=$(ibmcloud is security-groups --output json | jq -r ".[] | select(.name==\"$SG_VPE_NAME\") | .id")
Create the Virtual Private Endpoint (VPE)
Create a VPE in your VPC (GW_REGION) targeting the Secrets Manager instance (SM_REGION) and allocate a reserved IP from your subnet.
ibmcloud is endpoint-gateway-create \
--name "$VPE_NAME" \
--vpc "$VPC_ID" \
--target "$SM_CRN" \
--target-type provider_cloud_service \
--new-reserved-ip "{\"subnet\":{\"id\":\"$SUBNET_ID\"}}" \
--sg "$VPE_SG_ID" \
--resource-group-name "$RG_NAME"
# Wait for the VPE to reach stable state
ibmcloud is endpoint-gateway "$VPE_NAME" --output json | jq -r '.lifecycle_state'
# Capture the VPE IP (used later for routing and hosts entry)
export VPE_IP=$(ibmcloud is endpoint-gateway "$VPE_NAME" --output json | jq -r '.ips[0].address')
### Verify the VPE
# Show security groups attached to the VPE (names and IDs)
ibmcloud is endpoint-gateway "$VPE_NAME" --output json | jq -r '.security_groups[]? | "\(.name) \(.id)"'
# Show the reserved IP address allocated to the VPE
ibmcloud is endpoint-gateway "$VPE_NAME" --output json | jq -r '.ips[0].address'
Secrets Manager Private Certificate Engine
Use the Private Certificate engine to create CAs and issue certificates. Configure the Secrets Manager CLI:
export SECRETS_MANAGER_URL="$SM_PKI_ENDPOINT"
Create a root certificate authority (CLI)
Example (minimal) command to create a root CA configuration:
ibmcloud secrets-manager configuration-create \
--config-type "private_cert_configuration_root_ca" \
--name "vpn-root-CA" \
--certificate-common-name "vpn.root.ca" \
--private-cert-max-ttl "3652d" \
--private-cert-format "pem" \
--private-cert-private-key-type "rsa" \
--private-cert-private-key-bits 2048 \
--private-cert-distribution-points-encoded true \
--private-cert-issuing-certificate-urls-encoded true
- The
name(herevpn-root-CA) is how you refer to this root CA in later steps. - Adjust fields like
common_name,max_ttl, and key settings to your requirements.
Create an intermediate CA signed by the root
Example command to create an intermediate CA configuration that is signed by the previously created root CA:
ibmcloud secrets-manager configuration-create \
--config-type "private_cert_configuration_intermediate_ca" \
--name "vpn-intermediate-CA" \
--certificate-common-name "vpn.int.com" \
--private-cert-signing-method "internal" \
--private-cert-issuer "vpn-root-CA" \
--private-cert-max-ttl "2556d" \
--private-cert-format "pem" \
--private-cert-private-key-type "rsa" \
--private-cert-private-key-bits 4096 \
--private-cert-distribution-points-encoded true \
--private-cert-issuing-certificate-urls-encoded true
Sign the intermediate CA:
ibmcloud secrets-manager configuration-action-create \
--name "vpn-root-CA" \
--config-action-action-type "private_cert_configuration_action_sign_intermediate" \
--config-action-intermediate-certificate-authority "vpn-intermediate-CA"
Create a certificate template
A template controls what kinds of private certificates can be issued.
Example command to create a template bound to the intermediate CA:
ibmcloud secrets-manager configuration-create \
--config-type "private_cert_configuration_template" \
--name "vpn-certificate-template" \
--private-cert-ca-name "vpn-intermediate-CA" \
--private-cert-allowed-domains "vpn.ibm.com" \
--private-cert-allowed-domains-template false \
--private-cert-allow_subdomains true \
--private-cert-server-flag true \
--private-cert-client-flag true \
--private-cert-key-type "rsa" \
--private-cert-key-bits 2048 \
--private-cert-max-ttl "365d"
Issue the VPN server certificate
Use the private certificates engine to issue a server certificate from the template:
ibmcloud secrets-manager secret-create \
--secret-name "vpn-server-cert" \
--secret-type "private_cert" \
--secret-description "VPN server certificate" \
--secret-ttl "365d" \
--private-cert-certificate-template "vpn-certificate-template" \
--certificate-common-name "server.vpn.ibm.com" \
- Capture the resulting secret CRN and set
VPN_CERT_CRNto that value.
Issue client certificates
Use the private certificates engine to issue a client certificate from the template:
ibmcloud secrets-manager secret-create \
--secret-name "vpn-client-cert" \
--secret-type "private_cert" \
--secret-description "VPN client certificate" \
--secret-ttl "365d" \
--private-cert-certificate-template "vpn-certificate-template" \
--certificate-common-name "client.vpn.ibm.com"
Capture the server and client certificate CRNs:
export VPN_SERVER_CERT_CRN="<CRN of VPN server certificate secret>"
export VPN_CLIENT_CERT_CRN="<CRN of VPN client certificate secret>"
Security Group for the VPN server
Allow OpenVPN from your public IP and all outbound.
ibmcloud is security-group-create "$SG_VPN_NAME" "$VPC_ID"
export SG_ID=$(ibmcloud is security-groups --output json | jq -r ".[] | select(.name==\"$SG_VPN_NAME\") | .id")
# Find your public IPv4 and allow UDP 1194
curl -4 ifconfig.me
ibmcloud is security-group-rule-add "$SG_ID" inbound udp --port-min 1194 --port-max 1194 --remote x.x.x.x/32
# Allow all outbound
ibmcloud is security-group-rule-add "$SG_ID" outbound all --remote 0.0.0.0/0
Create the Client-to-Site VPN server
Certificate-based client authentication against your CA; server presents the issued server certificate.
ibmcloud is vpn-server-create \
--name "$VPN_NAME" \
--vpc "$VPC_ID" \
--subnet "$SUBNET_ID" \
--client-ip-pool "$CLIENT_POOL" \
--protocol udp \
--port 1194 \
--enable-split-tunnel true \
--client-auth-methods certificate \
--client-ca "$VPN_CLIENT_CERT_CRN" \
--cert "$VPN_SERVER_CERT_CRN" \
--sg "$SG_ID" \
--resource-group-name "$RG_NAME"
# Wait for the VPN server to reach stable status
ibmcloud is vpn-server "$VPN_NAME"
# Capture IDs and server private IP
export VPN_ID=$(ibmcloud is vpn-servers --output json | jq -r ".[] | select(.name==\"$VPN_NAME\") | .id")
export VPN_PRIV_IP=$(ibmcloud is vpn-server "$VPN_ID" --output json | jq -r '.private_ips[0].address')
### Allow TCP 443 from the translated source (VPN server private IP) to the VPE.
ibmcloud is security-group-rule-add "$VPE_SG_ID" inbound tcp --port-min 443 --port-max 443 --remote "$VPN_PRIV_IP/32"
Push a host route to the VPE
# Create a translated (SNAT) host route to the VPE IP
ibmcloud is vpn-server-route-create "$VPN_ID" --name vpe-host --action translate --destination "$VPE_IP/32"
# Verify for stable status
ibmcloud is vpn-server-routes "$VPN_ID"
Name resolution (example only)
Map the Secrets Manager hostname to the VPE IP on your local machine.
echo "$VPE_IP $SM_HOST" | sudo tee -a /etc/hosts
sudo dscacheutil -flushcache; sudo killall -HUP mDNSResponder
Prepare the OpenVPN Connect client
Download the server profile:
- In the IBM Cloud console left navigation menu, select Infrastructure > Network > VPNs.
- On the VPNs for VPC page, select the Client-to-Site servers pane and select
test-vpn. - On the
test-vpnpage, select the Clients tab and click theAll client profilesbutton to download the client profile .ovpn files in ZIP format. - Extract the ZIP file and import
client.ovpninto OpenVPN Connect and connect.
When you modify VPN routes, disconnect and reconnect the VPN client to receive the updated route configuration.
Validate from macOS
curl -v https://$SM_HOST- If it times out:
- Confirm the client has a route to
$VPE_IP/32and the VPN is connected. - Confirm the route action is translate and the VPE Security Group allows
$VPN_PRIV_IP/32 -> $VPE_IP/32on TCP 443 (or your chosen source). - Check VPN server and VPE lifecycle states are
stable/ok.
- Confirm the client has a route to
- Using your browser, open the Secrets Manager private-only UI.
Troubleshooting on macOS (routing and VPN)
Common symptoms in Mac:
curl -v https://$SM_HOSThangs and times out.curlfails immediately with errors likeNetwork is downorCannot allocate memorywhen trying to reach$VPE_IP.
Steps to diagnose and fix:
- Ensure only the IBM C2S VPN is active
- Disconnect any other VPN clients on the Mac (corporate, personal, etc.).
- Connect the OpenVPN profile for
test-vpnand wait until it shows as connected.
- Check the route to the VPE IP
route -n get "$VPE_IP"
Expected for a healthy setup:
interfaceis autundevice (for exampleutun5).gatewayis the tunnel peer address (for example10.241.x.yor172.30.x.y).- If
interfaceis something likeen0/en4instead ofutunX, the traffic is bypassing the VPN.
If the route is wrong, override it to use the VPN tunnel with the IBM VPN connected and other VPNs disabled:
# Remove any existing host route for the VPE IP
sudo route delete "$VPE_IP" 2>/dev/null || true
# Add a host route via the VPN tunnel interface (replace utun5 with your actual utun)
sudo route add -host "$VPE_IP" -interface utun5
# Verify
route -n get "$VPE_IP"
If you see interface: utun5 (or similar), retry:
curl -v --max-time 10 "https://$SM_HOST"
If curl still times out:
-
Double-check that the VPN server and VPE are in
stablestate:ibmcloud is vpn-server "$VPN_ID" --output json | jq -r '.lifecycle_state' ibmcloud is endpoint-gateway "$VPE_NAME" --output json | jq -r '.lifecycle_state' -
Confirm the VPE Security Group still has the inbound TCP 443 rule from
$VPN_PRIV_IP/32:ibmcloud is security-group-rules "$VPE_SG_ID"
Next steps
To enhance this configuration for production use, consider the following improvements:
- Replace
/etc/hostswith IBM Cloud DNS Services private zones for proper DNS resolution - Implement high availability by adding a second subnet and zone
- Configure Certificate Revocation List (CRL) handling for client certificates
- Tighten Security Group rules to follow the principle of least privilege
- Enable multi-factor authentication (MFA) for additional security
- Establish client certificate revocation policies
Security considerations
When implementing this VPN solution, keep the following security best practices in mind:
- Certificate management: Regularly rotate VPN certificates and implement automated renewal processes
- Access control: Limit VPN access to only the IP addresses and users that require it
- Monitoring: Enable logging and monitoring for VPN connections and access attempts
- Network segmentation: Use Security Groups to enforce strict network segmentation
- Encryption: Ensure all traffic uses strong encryption protocols (TLS 1.2 or higher)