OCI Data Safe: Database Security Assessment, Auditing, and Data Masking with Terraform

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

This site uses Akismet to reduce spam. Learn how your comment data is processed.