Software Architecture & Systems Design

Build or Buy? Surface Security and Privacy Trade-Offs

A cheap deployment pipeline and a purchased .NET platform can look like opposite bets. They often create the same security problem: someone else can see production data or influence the code that reaches it, and the team discovers this only after the first incident. I would buy commodity infrastructure, but keep identity, data access, and deployment authority under controls the company can inspect and revoke.

The cheapest pipeline can be the most expensive place to expose data

Cheap CI/CD vs Doing It Right What a CTO Should Buy or Build puts cost at the center of the decision, but I would treat the pipeline’s access to secrets and customer data as the first constraint because a successful build is little consolation if its logs disclose a production credential. The exposure is rarely an obvious “print secret” command. Integration tests upload diagnostic archives; a failed deployment preserves an environment dump; an engineer copies a production database into a test fixture. The pipeline then becomes a second, less-governed home for sensitive data.

Buying GitHub Actions or Azure DevOps usually beats operating your own runners when the workload can use short-lived jobs and tightly scoped credentials, because the provider maintains orchestration you would otherwise have to patch. Buying does not settle who can read artifacts, approve a workflow change, or issue a deployment token. GitHub documents a 90-day default retention period for artifacts, subject to repository and organization settings. I would set retention for sensitive test output to 7 days as an initial policy to tune, then shorten it if incident investigation does not require that history.

The first useful audit is a build, not a questionnaire. Run a representative job with synthetic records, deliberately fail a test, and inspect the logs, artifacts, caches, and notifications visible to an ordinary contributor. Record which identities can retrieve each copy. This catches disclosures that a vendor security page cannot see, because the vendor does not know what your test suite writes to standard output. For a quantitative starting point, count sensitive-looking output across a sample of 40 recent runs; the count is a measurement to make in your environment, not a claim about a typical pipeline.

I would not put long-lived production cloud keys in repository secrets, because a workflow permitted to read those keys may pass them to an unreviewed dependency or an altered build step. Prefer OpenID Connect (OIDC) federation to a cloud role constrained by repository, branch, and environment claims. Make deployment a separate job with protected-environment approval, since test code needs to prove it passes, not acquire production authority. GitHub Actions’ permissions setting can reduce the job token’s scope; the following workflow runs from a repository with a .NET solution at its root and grants no write permission to that token:

name: verify
on: [push]
permissions:
  contents: read
jobs:
  test:
    runs-on: ubuntu-24.04
    steps:
      - uses: actions/checkout@v4
        with:
          persist-credentials: false
      - uses: actions/setup-dotnet@v4
        with:
          dotnet-version: '10.0.x'
      - run: dotnet test --nologo

The version tags make the example readable, but a production review should pin third-party actions to reviewed commit SHAs because a moving tag can change what executes without changing your workflow file. That is a supply-chain control, not a substitute for reviewing the code those actions run.

Buying a .NET platform does not transfer responsibility for its data paths

Build .NET In-House or Buy It How Much Can You Cut Corners? poses a useful economy question. I would draw the boundary differently: buy repeatable hosting and observability, but retain design authority over where application data, encryption keys, and administrative identities travel. A vendor can operate Azure App Service competently while your application still sends personal data to an unexpected Application Insights property or stores an ASP.NET Core Data Protection key ring somewhere ephemeral.

Those failures have different consequences. Telemetry containing email addresses or authorization headers expands the population and retention of personal data because support staff and analytics integrations may now access it. A lost Data Protection key ring can invalidate authentication cookies after a restart; a key ring copied into the wrong environment can weaken the separation between those environments. Ask where keys persist, who can decrypt them, how rotation works, and whether staging and production have separate key rings. “Encrypted at rest” does not answer those questions because the operator holding decryption access may still be the person you meant to exclude.

For a purchased service, request a data-flow diagram covering request bodies, traces, backups, support access, subprocessors, and deletion. Test it against a real feature, such as a failed sign-in, because diagrams drawn solely from infrastructure inventories miss fields emitted by application logging. If a processor handles personal data on your behalf, GDPR Article 28 makes the processing arrangement relevant; Article 32 makes the security measures relevant. A SOC 2 Type II report may help assess a vendor’s controls, but it cannot prove that your developers have stopped logging session tokens.

Price this boundary explicitly. A vendor that provides managed key storage and access logs may be worth more than a cheaper host if it lets your team verify who accessed a secret. Conversely, I would reject a platform whose support staff require standing production database access, because contractual assurances offer less protection than access that does not exist. Grant time-limited access for a specific incident, capture the approval and query trail, and revoke it afterward.

Managed runners win routine builds; isolated runners win constrained data flows

The useful comparison is GitHub-hosted runners versus self-hosted ephemeral runners. GitHub-hosted runners win when tests can use synthetic data and OIDC credentials, because you avoid maintaining runner images and cleanup while keeping jobs disposable. Self-hosted ephemeral runners win when a build must reach a private dependency or a tightly controlled network segment, because you can constrain egress and keep execution within a network you administer. The first option costs provider minutes and requires confidence in external execution boundaries; the second costs patching, image maintenance, capacity, and investigation time.

Self-hosting is not automatically the privacy choice. A persistent runner can retain workspaces, credentials, or caches between jobs; an ephemeral runner that is destroyed after each job limits that carryover. Neither choice makes it safe to build untrusted pull requests with production network access. Separate untrusted validation from privileged deployment, since a malicious contribution can modify build scripts before a human reviews the application code. If a self-hosted runner has a route to the database, that route is part of the threat model even when the workflow never intends to use it.

Set a proposed maximum cloud-role session of 15 minutes for deployment and tune it against observed release duration. Short sessions narrow the period in which a stolen credential remains useful, although they cannot undo a deployment already performed. For provenance, SLSA v1.0 offers a vocabulary for describing how an artifact was built; Sigstore cosign can sign an artifact; and a CycloneDX 1.6 or SPDX 2.3 software bill of materials can record its components. None establishes that the source is safe. They are useful because they let an incident responder connect a deployed binary to a build, inputs, and dependencies rather than guessing which package shipped.

Apply the same discipline to NuGet restores. Commit lock files where appropriate and use NuGet’s RestoreLockedMode in CI, because a restore that resolves different transitive versions can make a reviewed commit produce an unexpected artifact. Run CodeQL or another suitable scanner, but treat findings as leads rather than clearance: a scanner cannot determine whether a legitimate-looking log statement violates your privacy promise. OWASP ASVS 4.0.3 can supply review requirements for authentication and session handling where automated checks leave gaps.

Access revocation should decide the contract before the invoice does

The late discovery that hurts most is often not “the vendor saw data.” It is “we cannot tell what the vendor saw, and we cannot remove their access without stopping releases.” Put that scenario into procurement. Require named human accounts through Microsoft Entra ID or another identity provider, auditable elevation, separate service identities, and a documented break-glass procedure. If the vendor proposes one shared administrator login, ask how you would attribute a database export to a person; if the answer is “we would ask the team,” the access model is inadequate.

Make exit rights operational rather than aspirational. Specify how the vendor returns build artifacts, deployment definitions, key-rotation records, and audit logs; specify how subcontractor access ends. Then rehearse replacing one vendor-operated credential with one you issue. The rehearsal exposes hidden coupling because a secret embedded in a vendor console is harder to migrate than a role assumption recorded in infrastructure code. It also distinguishes a vendor that administers your system from one whose proprietary account effectively owns it.

Set a proposed 24-hour limit for revoking a departing vendor engineer’s human access, then verify the limit with an actual offboarding test. Record the elapsed time as an observed result rather than accepting the contractual target as evidence. For privileged emergency access, require a reason, an expiry, and a record of actions taken. These demands have a cost: vendor onboarding takes longer and some convenient support paths disappear. They are still preferable to discovering after a breach that nobody can identify or disable the account involved.

I would accept a higher recurring vendor price for verifiable access boundaries, but not for a security badge alone, because the boundary determines what an attacker or mistaken operator can actually reach. The same rule applies in-house: hiring engineers does not create accountability unless their privileges, logs, and deployment approvals are equally clear.

A failed build should be the first procurement test

Before signing or renewing, give each option the same synthetic-data build and force it to fail. Ask a developer and a vendor operator to retrieve its logs and artifacts, then revoke one operator’s access and repeat the attempt. Record what remained visible, for how long, and under whose identity. That exercise produces a concrete purchasing requirement: pay for the option whose access you can prove, limit, and end.