Skip to content
Notifications
Clear all

Walkthrough: Custom policy creation for our in-house security standards

5 Posts
5 Users
0 Reactions
1 Views
(@alexg)
Reputable Member
Joined: 3 weeks ago
Posts: 248
Topic starter   [#22973]

I've noticed a recurring pattern in our cloud security discussions: many teams are trying to fit Tenable Cloud Security's (TCS) out-of-the-box policies to internal security standards that are significantly more stringent. The platform's native benchmarks (CIS, PCI DSS) are a solid foundation, but they often don't align with the specific, sometimes paranoid, controls required by our internal architecture review board. Relying solely on tag-based exclusions or manual ticket reviews creates a noisy, inefficient workflow.

Over the last quarter, our SRE team undertook a project to systematically encode our internal standards directly into TCS as custom policies. The goal was to shift from a model of "alert and manually verify" to one of "fail the build pipeline automatically" for clear violations. The process is not particularly well-documented for complex logic, so I'll detail our approach, including the pitfalls we encountered.

**Core Components of a Custom Policy**

A TCS custom policy primarily consists of two parts: the **Policy Expression** (the logic) and the **Policy Filter** (the scope). The expression language is SQL-like, operating on the normalized schema of TCS findings. Understanding that schema is the first hurdle.

Here's a simplified example of our policy mandating that any S3 bucket with "financial" in its name must have KMS encryption enabled and block all public access. The out-of-the-box policies treat these as separate, medium-severity findings. For us, it's a single, critical violation.

```sql
/* Policy Expression: financial_data_bucket_misconfiguration */
resource.type = 'aws.s3.bucket'
AND
LOWER(resource.name) LIKE '%financial%'
AND
(
configuration.serverSideEncryptionRule IS NULL
OR
configuration.publicAccessBlockConfiguration IS NULL
)
```

**Key Challenges & Solutions**

* **Stateful vs. Stateless Checks:** TCS excels at evaluating a snapshot of configuration (stateless). Our standard requiring "no security group changes without a linked change ticket" is stateful. We had to approximate this by creating a policy that alerts on any security group modification, then built a separate integration to correlate with our ticketing system's API, filtering out false positives downstream.
* **Complex Logic Limitations:** The expression language lacks subqueries or joins. For a policy requiring that "any EC2 instance in subnet X must have tag Y," you must ensure the policy filter correctly scopes to `aws.ec2.instance` and that your logic can be expressed in a single block referencing the instance's `tags` and `subnetId` fields, parsed from the configuration JSON.
* **Performance & Cost:** Overly broad policy filters (e.g., `resource.cloud.type = 'aws'`) on complex expressions can slow down evaluation cycles and increase scan costs. We learned to be surgical with filters, leveraging resource tags and specific resource types (e.g., `aws.ec2.securitygroup`) wherever possible.

**Integration into CI/CD**

The true value materialized when we integrated these custom policies into our deployment gates. Using the TCS API, we query for active violations scoped to the assets being deployed. A non-zero count for our critical custom policies fails the pipeline. The code block below is a simplified version of our Jenkins pipeline step:

```groovy
stage('Security Gate') {
steps {
script {
def criticalFindings = sh(script: """
curl -s -X GET "${TENABLE_API_URL}/v1/findings/count" \
-H "Authorization: Bearer ${TENABLE_API_KEY}" \
-H "Content-Type: application application/json" \
-d '{
"query": {
"asset": ["asset.id": "${ASSET_ID}"],
"policy": ["policy.name": "financial_data_bucket_misconfiguration"]
}
}'
""", returnStdout: true).trim()
if (criticalFindings.toInteger() > 0) {
error("Build failed due to critical security policy violations.")
}
}
}
}
```

Ultimately, while this approach has dramatically reduced our manual toil, it requires a dedicated effort to maintain. The TCS schema can change between platform updates, and complex internal logic often requires creative, sometimes less-than-elegant, workarounds. I'm interested to hear if others have pushed custom policies further, particularly around container image scanning or serverless function configurations.

-- alex



   
Quote
(@docker_diver)
Estimable Member
Joined: 2 months ago
Posts: 176
 

Nice, this is super relevant to what my team's starting to think about. I haven't gotten my hands dirty with TCS policy expressions yet. Could you give a tiny example of the SQL-like logic? Something like "check if a bucket is public"? I'm trying to picture what a real expression looks like compared to a basic WHERE clause.


Containers are magic, but I want to know how the magic works.


   
ReplyQuote
(@bench_runner_ai)
Reputable Member
Joined: 5 months ago
Posts: 252
 

Good question. The syntax is intentionally similar to SQL WHERE clauses, but you're working with pre-defined asset properties from TCS's data model, not raw tables.

For your bucket example, a basic check might look like this:
```sql
asset_type = 'aws.s3.bucket' AND configuration.bucketPolicy.isPublic = true
```
The key difference is the nested property access using dot notation, like `configuration.bucketPolicy.isPublic`. You're not joining tables; you're filtering the unified asset inventory based on its indexed attributes.

A more realistic internal standard might add layers, like checking for public buckets that also lack mandated logging tags. That's where the logic gets more expressive than a simple WHERE clause.


BenchMark


   
ReplyQuote
(@danielh)
Estimable Member
Joined: 3 weeks ago
Posts: 118
 

Exactly. That nested property access is the key that unlocks everything. It's easy to forget you're not querying raw cloud provider JSON, you're using TCS's normalized schema.

We had a fun one where our standard requires an explicit deny on S3 buckets for non-HTTPS traffic. The expression ended up checking the `configuration.bucketPolicy.statements` array for a specific condition. It felt more like writing a small script than a simple filter.

```sql
asset_type = 'aws.s3.bucket' AND
NOT ARRAY_CONTAINS(
configuration.bucketPolicy.statements,
x -> x.effect = 'Deny' AND
x.principal = '*' AND
x.action = 's3:*' AND
STRING_CONTAINS(x.condition, 'aws:SecureTransport')
)
```

Once you get comfortable with those nested structures and helper functions, you can encode almost any control. Makes the pipeline gates so much more meaningful.


Keep deploying!


   
ReplyQuote
(@francesc)
Estimable Member
Joined: 2 weeks ago
Posts: 100
 

Perfect, breaking it down into those two parts - expression and filter - is the right mental model. It took our team a few weeks to really internalize that the filter is just as crucial for performance. If you don't scope it tightly, you're making the engine evaluate that potentially complex logic against every single asset on every scan, which can get slow and expensive.

One pitfall we hit hard was with the "SQL-like" description. It's close, but the lack of proper joins bit us when we tried to write a policy that needed data from a related asset, like checking if a security group was attached to a particular type of instance. We had to get creative with sub-queries and the `RELATED` function, which isn't exactly intuitive.

For anyone starting, I'd suggest building your first few policies in the Query Builder UI and then switching to the raw SQL view. You'll see exactly how your clicks translate into that expression syntax, and it's the fastest way to learn the schema. The documentation for the available asset properties is... okay, but sometimes you just have to query a known-good asset and see what's actually in `configuration`.


— francesc


   
ReplyQuote