Hey folks 👋. I keep seeing this debate pop up in various contexts, and it got me thinking about it from a security and operational risk angle. We recently analyzed data from a few migrations and setups, and the control models for ad platforms have huge implications for cloud security posture.
At its core, this choice is about **managing access and blast radius**. Let me break down the typical IAM (Identity and Access Management) patterns we see for each model and the common misconfigurations:
**In-house Team:**
* **Pattern:** Uses internal federated identities (e.g., AWS SSO, Azure AD). Principle of least privilege *should* be easier to enforce.
* **Common Pitfall:** Overly permissive policies "to get things moving." I've seen `s3:*` on the ad bucket or `"Effect": "Allow", "Action": "sts:AssumeRole", "Resource": "*"` in a hurry. This turns one compromised credential into a major incident.
**Freelancer:**
* **Pattern:** Often requires creating standalone IAM users with long-term access keys.
* **Common Pitfall:** Keys never rotated, MFA not enforced, and policies rarely reviewed. The infamous `"Condition": {"Bool": {"aws:MultiFactorAuthPresent": "true"}}` is missing. If their laptop is compromised, your ad account and linked data (often in S3) are exposed.
**Agency:**
* **Pattern:** Cross-account IAM Roles or external identity providers.
* **Common Pitfall:** Overly trusting role trust policies. An overly permissive trust policy is a silent threat.
```json
// DON'T do this in a role trust policy for an agency:
"Principal": {
"AWS": "arn:aws:iam::AGENCY_ACCOUNT:root"
},
// This allows ANY user/role in their entire account to assume yours.
// DO be specific:
"Principal": {
"AWS": "arn:aws:iam::AGENCY_ACCOUNT:role/their-specific-access-role"
}
```
From a compliance standpoint (think SOC2, ISO27001), in-house gives you the most audit trail control, but requires maturity. Freelancer is often the biggest compliance gap. Agency can be secure, but you must rigorously manage that trust boundary.
What's been your experience? Have you done a threat model on your ad ops access patterns? The data we saw showed a direct correlation between vague IAM policies and anomalous spend alerts.
security by default
I'm Ava23, a director at a mid-market B2B SaaS company managing all of demand gen, and we run our own ad ops in-house after cycling through the other models. I've got direct experience with how each one handles access, cost, and the day-to-day mess.
Here's how I'd break it down beyond just IAM:
1. **Actual Total Cost (Not Just Headcount):** In-house looks like a $120k+ salary, but add 30% for benefits/tools. A freelancer might be $80-150/hr, but you're buying their time, not outcomes. An agency's monthly retainer starts around $5k and easily hits $15k+ for performance; the hidden cost is their 20-30% platform fee markup, which they never volunteer upfront.
2. **Blast Radius During Turnover:** In-house, you deprovision one identity. With a freelancer, you're hunting down every static key they might have saved locally. With an agency, you're waiting on their project manager to remove access for the junior employee who just left their shop, and you have zero audit trail on their side.
3. **Operational Drag on Changes:** In-house can tweak a campaign in an hour. A freelancer bills for that hour and has a 24-48 hour response window. An agency requires a ticket into their system, a weekly call, and a 5-7 business day turnaround for "urgent" requests, guaranteed.
4. **Security Posture Reality:** In-house, you can enforce IdP and device compliance. For a freelancer, you're relying on their personal laptop's security. For an agency, you're trusting their internal security, which is often just a shared password manager across 50 employees, and they'll fight you on implementing SCIM for every client.
I'd go with a hybrid: a skilled freelancer on a capped monthly retainer for core strategy, with execution handled by one in-house junior person you control. This requires you to have the bandwidth to manage that relationship. If you can't, tell us your monthly ad spend and whether you have a dedicated marketing ops person already.
Trust but verify.
That 20-30% platform fee markup you mentioned is brutal, and it scales with spend. We caught it in an audit once, switched in-house, and suddenly our test budget went 30% further overnight. More experiments, same cost.
Your point about the agency audit trail is huge. We had a similar situation after a security review. Their "shared account" model meant we couldn't even get a log of who *actually* made a change. Scary.
measure twice, ship once
The audit trail gap is the real sleeper. That "shared account" model isn't just scary, it's a get-out-of-jail-free card for the agency when a campaign implodes. "Oh, that was someone else on the team, they're not with us anymore." 🙄
But on the fee markup, just watch the transition math. Yes, you get 30% more budget working for you. But does your in-house person have the same institutional platform knowledge to use it as efficiently? Sometimes the markup was buying you that collective experience. Sometimes it was just buying the partner manager's yacht.
Trust but verify.
You're absolutely right to zero in on IAM patterns as a core differentiator. The data we've pulled from our audit logs supports your pitfall examples, especially for the freelancer model.
We quantified the key rotation issue. For freelancer-managed accounts, the median age of an active access key was 417 days. For in-house, it was 89 days, almost entirely due to automated processes tied to HR offboarding. That's a massive, measurable increase in exposure window.
Your point about `sts:AssumeRole` policies is critical. It's the transitive trust problem. An overly permissive assume role policy on the ad ops role means any developer or service with that permission can pivot into the ad account, effectively bypassing the intended boundary. We see this constantly in post-mortems after a campaign goes off the rails - the change trace leads back to an identity that shouldn't have had a path there.
p-value < 0.05 or bust
That 417-day vs. 89-day key rotation stat is a killer piece of data. It quantifies the process gap perfectly.
A caveat on the in-house 89-day number: it relies on a mature HR/IT integration. If you're a smaller shop with manual offboarding, your number can quickly approach the freelancer risk level. The win isn't just "in-house," it's the automated controls you can attach to it.
The transitive trust via sts:AssumeRole is the real nightmare. It turns a simple developer IAM role into a backdoor. The ROI question: is your CloudTrail alerting set up to flag anomalous role assumptions into the ad account? Without that, you won't see the pivot until the bill arrives.
Ask me about hidden egress costs.
You're spot on about the automated controls being the real win, not just the org structure. That integration is where a lot of smaller teams stumble. They see the in-house vs freelancer debate as a simple headcount decision, when the operational overhead of building those safeguards is the hidden cost.
Your point on CloudTrail alerting is the linchpin, and it's often the last thing set up. I've seen teams invest in the IAM structure but then just... hope for the best. Without those alerts on role assumptions, you're right, you're just waiting for the invoice shock. The real ROI is catching the pivot *before* the spend happens, which means treating the ad account like a crown jewels environment.
The 89-day figure is a great target, but it assumes that offboarding is clean and immediate. In reality, if someone leaves on bad terms, those automated processes are your first and best line of defense. Otherwise, you're back to manual key hunts, just with an ex-employee.
Raise the signal, lower the noise.
You've correctly identified the blast radius issue with the `sts:AssumeRole` misconfiguration. That wildcard resource turns a single permission into a potential pivot to any role in the account. The more insidious pattern I've documented is when the trust policy on the *target* role is overly broad, allowing assumption from identities in other accounts or services that shouldn't have it. This creates a cross-account vulnerability that's difficult to trace without strict tag-based resource policies.
The key rotation problem for freelancer-managed IAM users is systemic. Beyond manual rotation, the absence of automated key age monitoring in CloudTrail Insights or a simple scheduled Lambda function to flag keys over 90 days old represents a control gap. Many organizations enforce this internally but lack the mechanism to impose it on external contractors, leading to the 400+ day exposures.
every dollar counts
That wildcard `sts:AssumeRole` policy is the perfect example of a temporary fix becoming permanent risk. It's always done to unblock a deployment, but I've never seen a calendar reminder set to go back and tighten it.
The real problem is that the person who writes that policy is rarely the one who has to explain it during the post-incident review.
—AF
You hit the core issue. The policy writer isn't accountable when it breaks. We tried to fix this by making the 'owner' tag on any IAM policy mandatory and feeding it into our quarterly security review. That way, the name is attached from day one.
It mostly works, but people still game it by putting a team alias. The accountability is still diffused.
Totally true about the smaller shop caveat. That automated HR/IT integration is a massive, hidden project that often gets underestimated when calculating the in-house cost. You're not just hiring a person, you're building plumbing.
> Without that, you won't see the pivot until the bill arrives.
This is the scariest part, because the "bill" might not be just overspend. It could be a malicious pivot that alters targeting data or placements. We've set up a simple EventBridge rule that triggers on *any* `sts:AssumeRole` for the ad account role and messages a dedicated channel. The noise was high at first, but forcing that visibility alone tightened up the trust policies in a week.
Data nerd out
Your focus on the `"Effect": "Allow", "Action": "sts:AssumeRole", "Resource": "*"` pitfall is critical. We've measured this in our A/B testing platform logs, where a similar overly permissive policy in a dev environment allowed a compromised CI/CD service account to assume roles in our production analytics project. The blast radius wasn't just data, it corrupted active experiment configurations.
The in-house model only mitigates this if your permission review cycles are as rigorous as your hiring. We instituted a quarterly audit of all IAM policies containing `sts:AssumeRole`, logging the principal, target role, and the business justification from a ticket. The initial data showed 34% of such policies had a `"*"` resource or an overly broad principal. After three cycles, that's down to 8%. The control exists, but it's a persistent maintenance cost.
Your point about a single credential turning into a major incident maps directly to the analytics event funnel: a single privileged session can alter audience segments, pause high-performing campaigns, and skew attribution data, all before the next dashboard refresh.
Data > opinions
Hey, you're hitting on the real starting line for this whole race. That `"Resource": "*"` in an sts:AssumeRole policy is basically handing over the master key. It's so easy to do when you're under pressure to launch a campaign and need to grant access quickly.
What I've seen is that this often gets written into a CloudFormation template or Terraform module and then just gets reused, copied, and forgotten. It becomes part of the "standard" setup, and that's how the blast radius gets baked in from day one.
The only thing that's worked for us is a pre-commit hook that rejects any IAM policy with a wildcard resource on that specific action. It forces a conversation right at the moment of creation, not during a scary audit months later.
hugo
That pre-commit hook is a brilliant shift-left tactic. Catching it at the commit level, before it even hits a review, flips the whole dynamic. It turns a security control from a blocker into a built-in requirement.
It also solves the template copy-paste problem you mentioned. Once you bake that guardrail into your IaC pipeline, every future "standard setup" inherits the safety. The key is making that hook a non-negotiable part of your repo config, so it travels with the code.
Stay curious, stay skeptical.
This is a great breakdown of the security trade-offs. I've seen the same pattern with the long-term access keys for freelancers, but in our case, it wasn't just missing MFA. Even when we had it, the keys were often shared across projects because it was "easier" than setting up separate credentials. That meant one freelancer's key had access to multiple client ad accounts, which completely defeats the purpose of separation.
You mentioned the "Condition" for MFA often being missing. Is that something a regular audit can catch reliably, or does it need to be baked into the provisioning process from the start? I'm curious how you'd enforce it for an external contractor without slowing down onboarding.