---
term: 'BaaS Security Hardening: CLPs, ACLs, and Master Key Protection'
seoTitle: 'BaaS Security Hardening: CLPs, ACLs & Master Key Protection'
headline: 'What is BaaS Security Hardening?'
slug: baas-security-hardening
category: auth-security
shortDefinition: 'BaaS security hardening is a layered set of controls — CLPs, ACLs, and key discipline — that closes the openings a backend ships with.'
relatedTerms:
  - class-level-permissions-clp
  - access-control-lists-acl
  - api-key-security
  - data-layer-vs-application-layer-security
contrastsWith:
  - data-layer-vs-application-layer-security
faq:
  - question: 'What is BaaS security hardening?'
    answer: 'The pre-launch pass that converts a permissive development backend into a locked production one: class-level permissions restricted per operation, per-object ACLs on user data, the master key confined to server code, schema changes frozen, rate limits set, and the configuration audited from a real client. Each control covers a different layer, and the layers are checked in sequence on every request.'
  - question: 'Is the application ID a secret?'
    answer: 'No — treat it as public. Client keys and app IDs ship inside every mobile binary and every page of JavaScript, where anyone can extract them. They identify the app; they do not authenticate the caller. Real protection comes from what the backend enforces after identification: class-level permissions, ACLs, and authentication — the layers that hold even when every client-side key is known.'
  - question: 'Why must the master key never ship in a client app?'
    answer: 'Because it bypasses every gate: class-level permissions, ACLs, protected fields, and authentication checks are all void for master-key requests. A key embedded in a client binary can be extracted by anyone with the download, turning your entire database public-read-write. The master key belongs in server-side code only — used per operation, never stored where a client can reach it.'
  - question: 'What is the difference between CLPs and ACLs?'
    answer: 'Scope. A class-level permission is one rule on the schema answering "who may run this operation on this class at all"; an ACL is data on each object answering "who may touch this row". Requests pass the class gate first, then the object gate, and either can deny. Hardening uses both: broad strokes on the schema, fine grain on the rows.'
  - question: 'How do you rotate a leaked master key?'
    answer: 'Immediately generate a new key in the platform dashboard, update every server-side consumer — Cloud Code, jobs, admin scripts, CI — and revoke the old key. Then audit what the leaked key may have touched while valid. Rotation should also be routine, not just incident response: keys age into logs, backups, and old laptops, so schedule rotation like certificate renewal.'
  - question: 'When should writes go through Cloud Code instead of the SDK?'
    answer: 'Whenever the rule needs logic, not just identity: multi-object invariants, pricing, inventory, anything involving money or quotas. The hardened pattern locks the class down so clients cannot write it directly, then exposes a Cloud Code function as the only doorway — validation, enrichment, and audit logging ride along, and the class gate guarantees nobody walks around the chokepoint.'
  - question: 'Do rate limits belong in a BaaS security setup?'
    answer: 'Yes — permissions decide who may call an endpoint; rate limits decide how often. Without them, valid credentials become a scraping or brute-force tool: login endpoints get credential-stuffed and open queries get harvested at line speed. Set stricter limits on authentication routes and expensive queries, and pair them with alerting so an anomalous traffic spike is a page, not a surprise invoice.'
  - question: 'How do you audit a BaaS security configuration?'
    answer: 'Test from outside, as an attacker would: with only the public client keys, attempt to read and write every class as an anonymous user, an authenticated user, and another tenant''s user. Anything that succeeds and should not is a finding. Repeat after schema changes, keep the permission matrix in version-controlled documentation, and review logs for master-key usage patterns that should not exist.'
codeLanguages: [javascript, dart, swift, kotlin]
externalAuthorities:
  - name: 'OWASP API Security Top 10'
    url: 'https://owasp.org/API-Security/'
  - name: 'OWASP Authorization Cheat Sheet'
    url: 'https://cheatsheetseries.owasp.org/cheatsheets/Authorization_Cheat_Sheet.html'
  - name: 'Backend security guide'
    url: 'https://docs.parseplatform.org/parse-server/guide/#security'
  - name: 'Back4app app security guidelines'
    url: 'https://www.back4app.com/docs/security/parse-security'
cta:
  title: 'Hardening as a checklist, not a project'
  text: 'On Back4app the whole hardening pass lives in one dashboard: class-level permissions as checkboxes, ACLs enforced on every request, the master key held server-side, plus Cloud Code for the privileged path — lock down a backend in an afternoon.'
  linkText: 'Start building free'
  linkUrl: 'https://www.back4app.com/signup'
author: 'Back4app Engineering'
publishedDate: '2026-08-05'
translationKey: baas-security-hardening
---

**BaaS security hardening is a layered set of controls — CLPs, ACLs, and key discipline — that closes the openings a backend ships with.** Backends are born permissive on purpose: open classes and client-writable schemas make prototypes fly. Hardening is the deliberate pass that flips those defaults before launch — and it is a short, known checklist, not a research project.

## Key takeaways

| Question | Answer |
| --- | --- |
| What it is | The pre-launch pass that locks class permissions, ACLs, keys, and schema |
| The layers | CLPs gate the class → ACLs gate the row → protected fields gate the column |
| The one absolute | The master key bypasses everything — it never leaves the server |
| The privileged path | Cloud Code functions are the audited doorway for sensitive writes |
| What keys are not | App IDs and client keys are identifiers, not secrets — plan accordingly |

## The hardened default, in code

The per-object half of the story — an owner-scoped ACL layered under a locked class:

**JavaScript:**

```javascript
// JavaScript / Node.js — Back4app JS SDK
// Per-object ACL: the owner reads and writes, a moderator role reads,
// the public gets nothing — layered under the class-level permissions
const note = new Parse.Object('Note');
note.set('body', 'quarterly numbers');

const acl = new Parse.ACL(Parse.User.current()); // owner: read + write
acl.setPublicReadAccess(false);
acl.setPublicWriteAccess(false);
acl.setRoleReadAccess('moderator', true);        // role: read only
note.setACL(acl);
await note.save();

// The class gate above it is schema config, set in the dashboard:
// find/get: requiresAuthentication · create: authenticated · addField: nobody
```

**Flutter:**

```dart
// Flutter / Dart — Back4app Flutter SDK
// Per-object ACL: owner read/write, moderator role read, public nothing
final note = ParseObject('Note')..set('body', 'quarterly numbers');

final acl = ParseACL(owner: await ParseUser.currentUser()); // owner: r+w
acl.setPublicReadAccess(allowed: false);
acl.setPublicWriteAccess(allowed: false);
acl.setReadAccess(userId: 'role:moderator', allowed: true); // role: read
note.setACL(acl);
await note.save();

// The class gate above it is schema config, set in the dashboard:
// find/get requiresAuthentication, create authenticated, addField off
```

**Swift:**

```swift
// iOS / Swift — Back4app Swift SDK
// Per-object ACL: owner read/write, moderator role read, public nothing
var note = Note()
note.body = "quarterly numbers"

var acl = ParseACL()
acl.publicRead = false
acl.publicWrite = false
if let user = User.current {
    acl.setReadAccess(user: user, value: true)   // owner: read
    acl.setWriteAccess(user: user, value: true)  // owner: write
}
acl.setReadAccess(roleName: "moderator", value: true) // role: read only
note.ACL = acl
note.save { result in
    if case .failure(let error) = result { print(error) }
}
```

**Kotlin:**

```kotlin
// Android / Kotlin — Back4app Android SDK
// Per-object ACL: owner read/write, moderator role read, public nothing
val note = ParseObject("Note").apply { put("body", "quarterly numbers") }

val acl = ParseACL(ParseUser.getCurrentUser()) // owner: read + write
acl.publicReadAccess = false
acl.publicWriteAccess = false
acl.setRoleReadAccess("moderator", true)       // role: read only
note.acl = acl
note.saveInBackground { e ->
    if (e != null) Log.w("ACL", "save failed: ${e.code}")
}

// The class gate above it is schema config, set in the dashboard:
// find/get requiresAuthentication, create authenticated, addField off
```

## CLP vs. ACL vs. master key: which control does what

| Control | Scope | Set where | Fails how |
| --- | --- | --- | --- |
| [Class-level permission](/glossary/class-level-permissions-clp/) | Whole class, per operation | Schema (dashboard) | Left at permissive defaults |
| [ACL](/glossary/access-control-lists-acl/) | One object | On each row, at save time | Forgotten on new objects |
| Protected fields | One column | Schema | Sensitive columns readable by peers |
| Master key | Bypasses all of the above | Server environment only | Shipped in a client binary |

The first three compose into a permission ladder every request climbs. The fourth is the ladder's fire escape — indispensable server-side, catastrophic anywhere else. And the identifiers apps do ship — the app ID and client [API keys](/glossary/api-key-security/) — belong in a different mental category entirely: they are extractable from any binary, so they identify the app rather than protect it. Assume they are public, and let the data layer do the enforcing.

## Where a hostile request dies

```mermaid
flowchart LR
  accTitle: The layered gates a hostile request must pass in a hardened BaaS backend
  accDescr: A request with extracted client keys passes identification, then hits rate limiting, then the class-level permission gate, then the per-object ACL, then protected-field filtering; each layer can deny it. A separate master-key path bypasses all gates, which is why the master key must stay server-side.
  A["Request with<br/>extracted client keys"] --> B["Rate limit"]
  B --> C{"CLP: may this caller<br/>run this operation?"}
  C -- no --> X1["Denied"]
  C -- yes --> D{"ACL: may this caller<br/>touch this object?"}
  D -- no --> X2["Denied"]
  D -- yes --> E["Protected fields<br/>stripped"] --> F["Data"]
  M["Master key<br/>(server only)"] -. bypasses every gate .-> F
```

The diagram is also the audit script: at each gate, ask what happens when an attacker with your public keys arrives as an anonymous user, as a logged-in user, and as a different tenant's user. Every "allowed" that surprises you is the finding.

## How to harden a BaaS backend

| Step | Action | What it closes |
| --- | --- | --- |
| 1 | Set every class's CLPs: `requiresAuthentication` minimum, role-scoped writes | The classic anonymous full-table dump |
| 2 | Turn off `addField` on all classes and client class creation | Schema drift and junk-class injection |
| 3 | Default ACL on user-owned objects: owner read/write, public nothing | Cross-user reads and writes |
| 4 | Protect sensitive columns (email, tokens, scores) with field-level rules | Peer users reading private attributes |
| 5 | Confine the master key to server environments; rotate on schedule and on any leak | Total-bypass exposure |
| 6 | Route sensitive writes through Cloud Code functions with locked classes behind them | Business-rule violations and tampering |
| 7 | Rate-limit auth endpoints and expensive queries | Credential stuffing and bulk scraping |
| 8 | Re-test from a real client with only public keys; log and review master-key usage | Config drift going unnoticed |

Steps 1–4 are [data-layer enforcement](/glossary/data-layer-vs-application-layer-security/) — declarative, checked on every path. Steps 5–8 are operational discipline. Both halves are necessary; neither is sufficient.

## Master key discipline and the privileged path

The master key exists because someone legitimate — migrations, admin tools, scheduled jobs — must be able to ignore the gates. Discipline means treating it like the root credential it is: injected into server environments as configuration, never committed, never logged, never embedded in anything a user downloads, and used per-operation rather than held as a session-wide default. Rotation is the underrated half — keys leak quietly into CI logs and old laptops, so rotating on a schedule (and instantly on any suspicion) caps the blast radius of a leak you never detect.

Cloud Code is the pattern that makes strict data-layer rules livable. Lock a class completely — no client writes at all — and expose a serverless function as the only doorway. The function validates input, enforces business rules the schema cannot express ("only before the order ships"), writes with elevated privileges, and leaves an audit trail. The class gate guarantees the chokepoint cannot be bypassed; the chokepoint keeps the logic reviewable in one place.

## Common use cases

- **The pre-launch lockdown.** The full checklist above, run once before real users arrive — the single highest-leverage security hour a small team spends.
- **Multi-tenant data isolation.** Owner-scoped ACLs plus authenticated-only CLPs so tenant A can never query tenant B, enforced below the application code.
- **Payments and inventory.** Classes locked to zero client writes, with Cloud Code functions as the audited path for anything touching money or stock.
- **Incident response.** A leaked key or a surprise in the logs triggers rotation, an access audit, and a re-run of the outside-in permission test.
- **Compliance evidence.** A version-controlled permission matrix and master-key usage logs turn "we take security seriously" into an artifact an auditor can read.

## Should you harden at the data layer or in application code? A decision matrix

| Enforce at the data layer (CLPs + ACLs) when… | Enforce in application code (Cloud Code) when… |
| --- | --- |
| The rule is about identity and ownership | The rule needs business logic or multi-object state |
| It must hold on every path, including future clients | One audited chokepoint is the requirement |
| A dashboard toggle is the whole specification | Validation, enrichment, or side effects ride along |
| You want security that survives app rewrites | The rule changes faster than the schema should |
| The failure mode of forgetting is catastrophic | The failure mode is a business bug, not a breach |

In practice the answer is layered, not either/or: data-layer rules as the floor that always holds, application-layer functions for everything conditional — the same request passes through both.

## Limitations and trade-offs

- **Hardening is configuration, and configuration drifts.** New classes arrive with permissive defaults; without a re-audit habit, last year's lockdown quietly erodes.
- **Declarative rules cannot express logic.** "Owners only" is a toggle; "only refundable within 30 days" is code — over-relying on the data layer pushes teams to contort schemas instead of writing a function.
- **Strictness has a developer-experience cost.** Locked classes and frozen schemas slow prototyping, which is why the discipline is staged: open in development, locked at launch.
- **Rate limits and audits need tuning.** Too-tight limits throttle legitimate spikes; unread logs are decoration. Both need an owner, not just a setup commit.
- **The layers protect data, not everything.** Dependency vulnerabilities, leaked user passwords, and social engineering live outside this model — hardening the data layer is necessary, not sufficient, as the [OWASP API Security Top 10](https://owasp.org/API-Security/) catalog makes clear.

## BaaS security hardening on Back4app

Back4app is an open-source Backend-as-a-Service (BaaS) platform that combines a managed database, auto-generated REST and GraphQL APIs, authentication, file storage, and Cloud Code serverless functions. The checklist in this article maps onto its dashboard almost one-to-one: class-level permissions and protected fields are checkboxes per class, ACLs are enforced server-side on every request across SDKs, REST, and GraphQL, and the master key stays in server configuration where Cloud Code — the privileged path — can use it per operation. The [security guidelines](https://www.back4app.com/docs/security/parse-security) walk the same pass step by step, so hardening a backend is an afternoon of toggles and one honest outside-in test.
