Most database security controls focus on the perimeter: who can connect, what ports are open, whether TLS is enforced. Those controls matter. But they say nothing about what happens inside the database once a connection is established. Which users have excessive privileges they never use? Which tables contain sensitive data that should be masked in non-production environments? Which queries are being run by service accounts outside of business hours? Without answers to those questions, you do not have database security. You have network security in front of a database.
OCI Data Safe answers those questions. It connects to Oracle databases running in OCI, on-premises, or in other clouds and provides security assessments, user risk analysis, activity auditing, sensitive data discovery, and data masking. This post covers registering databases with Data Safe using Terraform, running security assessments, setting up audit policies, discovering sensitive data, and generating masked copies for non-production use.
Architecture
OCI Data Safe is a regional service that connects to target databases over the database listener port. For databases inside OCI VCNs, the connection uses a private endpoint. For on-premises databases, it connects through FastConnect or VPN. The Data Safe service does not store your data. It reads database metadata, user configurations, and audit logs. The actual masking operations run inside the database using temporary procedures that Data Safe generates and executes.
Step 1: IAM Policy for Data Safe
resource "oci_identity_policy" "data_safe_policy" {
compartment_id = var.compartment_id
name = "data-safe-management-policy"
description = "Permissions for OCI Data Safe service and operators"
statements = [
# Enable Data Safe service
"Allow service datasafe to use network-family in compartment id ${var.compartment_id}",
# Security and DBA teams can manage Data Safe
"Allow group ${var.dba_group_name} to manage data-safe-family in compartment id ${var.compartment_id}",
"Allow group ${var.security_group_name} to read data-safe-family in compartment id ${var.compartment_id}",
"Allow group ${var.security_group_name} to manage data-safe-assessment-family in compartment id ${var.compartment_id}",
# Data Safe needs to read database details
"Allow service datasafe to read autonomous-database-family in compartment id ${var.compartment_id}",
"Allow service datasafe to read database-family in compartment id ${var.compartment_id}"
]
}
Step 2: Enable Data Safe and Register Target Databases
# Enable Data Safe for the tenancy
resource "oci_data_safe_data_safe_configuration" "enable" {
compartment_id = var.tenancy_ocid
is_enabled = true
}
# Register an Autonomous Database as a Data Safe target
resource "oci_data_safe_target_database" "production_adb" {
compartment_id = var.compartment_id
display_name = "production-autonomous-db"
description = "Production ADB registered with Data Safe"
database_details {
database_type = "AUTONOMOUS_DATABASE"
autonomous_database_id = var.autonomous_database_id
infrastructure_type = "ORACLE_CLOUD"
}
connection_option {
connection_type = "PRIVATE_ENDPOINT"
datasafe_private_endpoint_id = oci_data_safe_private_endpoint.production_pe.id
}
credentials {
user_name = "DATASAFE_ADMIN"
password = var.datasafe_admin_password
}
defined_tags = {
"Operations.Environment" = "production"
"Operations.ManagedBy" = "terraform"
}
}
# Private endpoint for Data Safe to connect to the database
resource "oci_data_safe_private_endpoint" "production_pe" {
compartment_id = var.compartment_id
display_name = "data-safe-private-endpoint"
subnet_id = var.private_subnet_id
vcn_id = var.vcn_id
nsg_ids = [var.datasafe_nsg_id]
description = "Private endpoint for Data Safe to access production databases"
}
output "target_database_id" {
value = oci_data_safe_target_database.production_adb.id
}
Before registering the database, create the Data Safe admin user inside the database. Data Safe uses this account to read metadata and execute operations.
-- Run this inside the target Autonomous Database as admin
CREATE USER datasafe_admin IDENTIFIED BY "YourStrongPassword123!";
-- Grant Data Safe required privileges
EXEC DBMS_CLOUD_ADMIN.GRANT_DATASAFE_ROLE('DATASAFE_ADMIN');
-- Verify the role was granted
SELECT GRANTED_ROLE FROM DBA_ROLE_PRIVS
WHERE GRANTEE = 'DATASAFE_ADMIN';
Step 3: Security Assessment
A security assessment evaluates your database configuration against Oracle security best practices and the CIS Oracle Database Benchmark. It produces a risk-rated report covering password policies, privilege assignments, audit settings, network encryption, and database configuration parameters.
resource "oci_data_safe_security_assessment" "production_baseline" {
compartment_id = var.compartment_id
target_id = oci_data_safe_target_database.production_adb.id
description = "Baseline security assessment for production ADB"
display_name = "production-security-assessment-baseline"
schedule = "weekly"
}
output "assessment_id" {
value = oci_data_safe_security_assessment.production_baseline.id
}
# Trigger an on-demand assessment via CLI
# oci data-safe security-assessment create \
# --compartment-id ${COMPARTMENT_ID} \
# --target-id ${TARGET_DB_ID}
# Get assessment results and risk counts
oci data-safe security-assessment get \
--security-assessment-id ${ASSESSMENT_ID} \
--query 'data.{state:"lifecycle-state",
high:statistics."high-risk",
medium:statistics."medium-risk",
low:statistics."low-risk",
passed:statistics."pass"}' \
--output table
Set up weekly scheduled assessments and compare each result against the baseline. Data Safe tracks which findings are new, which are resolved, and which are deferred. A new high-risk finding since the last baseline is a signal that something changed in the database configuration that warrants investigation.
resource "oci_monitoring_alarm" "security_assessment_high_risk" {
compartment_id = var.compartment_id
display_name = "datasafe-high-risk-findings"
is_enabled = true
metric_compartment_id = var.compartment_id
namespace = "oci_data_safe"
query = "SecurityAssessmentHighRiskFindings[1d]{targetId = '${oci_data_safe_target_database.production_adb.id}'}.max() > 0"
severity = "CRITICAL"
pending_duration = "PT24H"
destinations = [var.security_notification_topic_id]
body = "Data Safe security assessment found new high-risk findings on the production database. Review the assessment report and remediate."
}
Step 4: User Assessment and Risk Analysis
A user assessment profiles every database user: their privileges, their last login time, their password age, whether they have DBA-equivalent roles they should not have, and whether they are service accounts that have never logged in interactively. This is the fastest way to find privilege creep in a long-running database.
resource "oci_data_safe_user_assessment" "production_users" {
compartment_id = var.compartment_id
target_id = oci_data_safe_target_database.production_adb.id
display_name = "production-user-assessment"
schedule = "monthly"
}
# Query users flagged as high risk after assessment completes
oci data-safe user-assessment-user list \
--user-assessment-id ${USER_ASSESSMENT_ID} \
--user-category HIGH_RISK \
--query 'data[*].{user:"user-name",
risk:"user-category",
admin-roles:"admin-roles",
last-login:"time-last-login",
password-age:"password-age-in-days"}' \
--output table
# Get users with DBA privilege who have not logged in for 90 days
oci data-safe user-assessment-user list \
--user-assessment-id ${USER_ASSESSMENT_ID} \
--query 'data[?contains("admin-roles", `DBA`) && "time-last-login" < `2026-05-01`].
{user:"user-name", last-login:"time-last-login"}' \
--output table
Step 5: Activity Auditing
Data Safe audit policies define which database activities to capture and ship to the Data Safe audit trail. Captured events include DDL operations, privilege grants, login failures, data access on sensitive tables, and administrative actions.
resource "oci_data_safe_audit_policy" "production_audit" {
compartment_id = var.compartment_id
target_id = oci_data_safe_target_database.production_adb.id
display_name = "production-audit-policy"
description = "Audit policy for production database"
audit_specifications {
audit_unique_name = "ORA_SECURECONFIG"
is_enabled_for_all_users = true
is_user_activity_tracked = true
}
audit_specifications {
audit_unique_name = "ORA_DATABASE_PARAMETER"
is_enabled_for_all_users = true
is_user_activity_tracked = true
}
audit_specifications {
audit_unique_name = "ORA_PRIVILEGE_CHANGES"
is_enabled_for_all_users = true
is_user_activity_tracked = true
}
audit_specifications {
audit_unique_name = "ORA_LOGON_FAILURES"
is_enabled_for_all_users = true
is_user_activity_tracked = true
}
audit_specifications {
audit_unique_name = "ORA_USER_CHANGES"
is_enabled_for_all_users = true
is_user_activity_tracked = true
}
}
# Provision the audit policy on the target database
resource "oci_data_safe_audit_policy_management" "provision" {
audit_policy_id = oci_data_safe_audit_policy.production_audit.id
provision_trigger = 1
}
# Alert on high number of login failures
resource "oci_monitoring_alarm" "login_failure_spike" {
compartment_id = var.compartment_id
display_name = "datasafe-login-failures-high"
is_enabled = true
metric_compartment_id = var.compartment_id
namespace = "oci_data_safe"
query = "AuditEventsCollected[5m]{targetId = '${oci_data_safe_target_database.production_adb.id}', eventType = 'LOGON_FAILED'}.sum() > 20"
severity = "WARNING"
pending_duration = "PT5M"
destinations = [var.security_notification_topic_id]
body = "More than 20 failed login attempts detected on the production database in the last 5 minutes. Possible brute force or credential stuffing attempt."
}
Step 6: Sensitive Data Discovery
Before you can mask sensitive data for non-production environments, you need to know where it lives. Data Safe sensitive data discovery scans your database schema and identifies columns that likely contain personal data, financial data, health information, or credentials based on column names, data types, and sample values.
resource "oci_data_safe_sensitive_data_model" "production_sdm" {
compartment_id = var.compartment_id
target_id = oci_data_safe_target_database.production_adb.id
display_name = "production-sensitive-data-model"
description = "Sensitive data model for production ADB"
# Include all schemas except system schemas
schemas_for_discovery = ["HR", "ORDERS", "CUSTOMERS", "FINANCE"]
# Sensitive type groups to discover
sensitive_type_ids_for_discovery = [
# Personal identifiers
"ocid1.sensitivitytype.oc1..national-identifier",
"ocid1.sensitivitytype.oc1..email-address",
"ocid1.sensitivitytype.oc1..phone-number",
"ocid1.sensitivitytype.oc1..date-of-birth",
# Financial data
"ocid1.sensitivitytype.oc1..credit-card-number",
"ocid1.sensitivitytype.oc1..bank-account-number",
# Health information
"ocid1.sensitivitytype.oc1..medical-record-number",
# Authentication data
"ocid1.sensitivitytype.oc1..password"
]
is_app_defined_relation_discovery_enabled = true
is_include_all_schemas = false
is_include_all_sensitive_types = false
}
# Trigger discovery
resource "oci_data_safe_discovery_job" "initial_discovery" {
compartment_id = var.compartment_id
sensitive_data_model_id = oci_data_safe_sensitive_data_model.production_sdm.id
discovery_type = "ALL"
}
# List discovered sensitive columns
oci data-safe sensitive-column list \
--sensitive-data-model-id ${SDM_ID} \
--query 'data.items[*].{schema:"schema-name", table:"object-name", column:"column-name", type:"sensitive-type-id", status:status}' \
--output table
Step 7: Data Masking for Non-Production Environments
Once sensitive columns are discovered, create a masking policy that defines how each column type should be transformed. Data Safe supports format-preserving masking, which maintains the structure of the data while replacing the values with realistic but fictitious equivalents.
resource "oci_data_safe_masking_policy" "staging_mask" {
compartment_id = var.compartment_id
sensitive_data_model_id = oci_data_safe_sensitive_data_model.production_sdm.id
display_name = "staging-masking-policy"
description = "Masking policy for staging environment data copies"
# Post-masking script to rebuild indexes and gather statistics
post_masking_script = "BEGIN DBMS_STATS.GATHER_SCHEMA_STATS('HR'); END;"
is_drop_temp_tables_enabled = true
is_redo_logging_enabled = false
is_refresh_stats_enabled = true
parallel_degree = 4
recompile = "FULL"
column_source {
column_source = "SENSITIVE_DATA_MODEL"
sensitive_data_model_id = oci_data_safe_sensitive_data_model.production_sdm.id
}
}
# Execute masking on the staging database
resource "oci_data_safe_masking_policies_masking_column" "email_masking" {
masking_policy_id = oci_data_safe_masking_policy.staging_mask.id
schema_name = "CUSTOMERS"
object_name = "CUSTOMER_PROFILES"
column_name = "EMAIL_ADDRESS"
sensitive_type_id = "ocid1.sensitivitytype.oc1..email-address"
masking_formats {
format_entries {
type = "RANDOM_EMAIL_FORMAT"
}
}
}
resource "oci_data_safe_masking_policies_masking_column" "phone_masking" {
masking_policy_id = oci_data_safe_masking_policy.staging_mask.id
schema_name = "CUSTOMERS"
object_name = "CUSTOMER_PROFILES"
column_name = "PHONE_NUMBER"
sensitive_type_id = "ocid1.sensitivitytype.oc1..phone-number"
masking_formats {
format_entries {
type = "RANDOM_DIGITS"
start_length = 10
end_length = 10
}
}
}
# Run the masking job
resource "oci_data_safe_masking_policy_health_report" "pre_mask_check" {
masking_policy_id = oci_data_safe_masking_policy.staging_mask.id
target_id = oci_data_safe_target_database.production_adb.id
}
# Execute masking via CLI after validation
# oci data-safe masking-job execute-masking \
# --masking-policy-id ${MASKING_POLICY_ID} \
# --target-id ${STAGING_TARGET_ID}
Run masking on a copy of the production database in staging, not on production itself. The masking operation modifies data in place. The standard workflow is: take a snapshot or export of production, restore it to the staging database, run Data Safe masking against the staging target. The staging database now contains realistic but non-sensitive data safe for developer use.
Operational Notes
Run security assessments after every major database change. Schema changes, new user grants, parameter modifications, and patch applications can all introduce new findings. A weekly scheduled assessment catches drift between changes. An on-demand assessment run immediately after a change confirms the change did not introduce a regression.
Sensitive data discovery needs to be re-run when the schema changes. New tables and columns added by application deployments are not automatically included in an existing sensitive data model. Schedule discovery jobs to run monthly or after major application deployments to keep the model current.
The Data Safe audit trail has a retention limit. Configure audit trail archiving to OCI Object Storage for long-term retention. Regulatory frameworks commonly require database audit logs to be retained for one to seven years. The Object Storage lifecycle policies covered in a previous post can manage the tiering and deletion of archived audit data automatically.
Regards,
Osama
#OCI #OracleCloud #DataSafe #DatabaseSecurity #Terraform #IaC #OracleCloudInfrastructure #DataPrivacy #DevSecOps #PlatformEngineering #TechBlog #Oracle #CloudSecurity #DataMasking #GDPR #DatabaseAudit #SensitiveData #Compliance #OracleDatabase #SecurityAssessment
Leave a comment