Workers' Day: The Work That Never Cuts Corners
Security Engineering Best Practices Nigeria

Workers' Day: The Work That Never Cuts Corners

Today is Workers’ Day. In Nigeria, it’s a public holiday — a pause, a moment to acknowledge labour. Most of the tributes will go to factory workers, nurses, civil servants. Very few will go to the engineers sitting in front of a laptop at 11 PM trying to decide whether to validate that input properly or just ship the feature.

This post is for those engineers. And it’s a challenge, not a celebration.

Because there’s a kind of work that doesn’t make the sprint demo. It doesn’t close a ticket with a satisfying green checkmark. No one claps for it in the standup. But it is — without question — the most important work you will ever do. It’s the work of building software that doesn’t betray the people who use it.

I want to make the case, clearly and without apology, that security-aware development is not a specialisation. It is not a phase you bolt on before launch. It is not the job of a dedicated “security team” that reviews your PRs once a quarter. It is craft. It is professionalism. And if you are shipping software that touches Nigerian users — people who are trusting you with their money, their identity, their data — it is a moral obligation.

The Honest Problem: We Cut Corners Because We’re Allowed To

Let me say the quiet part out loud.

Most insecure software does not exist because engineers didn’t know better. It exists because no one stopped them. The product manager wanted the feature by Friday. The investor wanted the demo by Monday. The startup wanted to move fast and not get slowed down by “engineering overhead.” And “let’s add proper input validation” lost to “let’s ship.”

I have been in those rooms. I have watched security work get deprioritised not because it was wrong, but because it was invisible. A SQL injection vulnerability that never got exploited looks exactly like code that was never written. Until it doesn’t.

The Nigerian startup ecosystem is building fast — genuinely fast, impressively fast. Fintech, healthtech, agritech — we are solving real problems at real scale. But speed without security is not velocity. It is debt. And in software, security debt compounds silently, and it pays out ugly.

Five Things That Are Not Negotiable

These are not advanced topics. They are not optional enhancements. They are the floor — the minimum bar for code that deserves to be in production.


1. Never Trust Input

Every boundary where data enters your system — a form field, a URL parameter, a webhook payload, an uploaded file, a header — is a potential attack surface. The rule is simple: validate everything, trust nothing.

This is not about being paranoid. It is about acknowledging that your users are not all well-intentioned, and even the well-intentioned ones make mistakes. SQL injection, XSS, path traversal — these attacks are not exotic. They are in the OWASP Top 10. They have been for twenty years. And they still succeed because someone, somewhere, trusted input they shouldn’t have.

# Insecure — raw user input concatenated into a query
def get_user(user_id):
    query = f"SELECT * FROM users WHERE id = '{user_id}'"
    return db.execute(query)

# Secure — parameterised query; the database handles escaping
def get_user(user_id: int):
    query = "SELECT * FROM users WHERE id = ?"
    return db.execute(query, (user_id,))

The same principle applies in JavaScript, C#, Go — every language, every framework. Parameterised queries. Schema validation on request bodies. Allowlists over denylists. Reject malformed input early and loudly at the boundary, not silently in the middle of your business logic.


2. Secrets Belong in Secrets Managers, Not in Code

I will keep this blunt: if your API key, database password, or JWT secret is in your repository — even in a .env file that you think is gitignored — you are one misconfiguration away from a serious breach.

GitHub’s secret scanning catches thousands of exposed credentials every day. The majority are not from sophisticated attacks. They are from developers who pushed a file they shouldn’t have, or copied a config into a test, or left a key hardcoded “just temporarily.”

There is no temporarily. There is only committed and not yet found.

# Insecure — secret hardcoded in source
DATABASE_URL = "postgresql://admin:P@ssw0rd123@prod-db.internal/app"

# Secure — loaded from environment at runtime
import os
DATABASE_URL = os.environ["DATABASE_URL"]

Use environment variables for local development. Use a secrets manager (Cloudflare Workers Secrets, AWS Secrets Manager, Azure Key Vault, HashiCorp Vault) in production. Add .env, .dev.vars, and any credential files to .gitignore — and verify they are actually ignored before your first commit.

Run git secret scan or integrate a tool like truffleHog into your CI pipeline. Treat an exposed secret as an incident, not a warning. Rotate it immediately.


3. Least Privilege Is Not a Configuration Detail

This one is subtle, and that is exactly why it gets ignored.

Least privilege means every component in your system — every database user, every IAM role, every API token, every service account — should have the minimum permissions it needs to do its job and nothing more. Not “probably won’t need more than this.” Not “we’ll tighten it up later.” The minimum, now, from day one.

# Insecure — application role with full table access
resource "aws_iam_role_policy" "app_policy" {
  policy = jsonencode({
    Statement = [{
      Action   = ["dynamodb:*"]
      Effect   = "Allow"
      Resource = "*"
    }]
  })
}

# Secure — scoped to exactly the operations the app needs
resource "aws_iam_role_policy" "app_policy" {
  policy = jsonencode({
    Statement = [{
      Action   = ["dynamodb:GetItem", "dynamodb:PutItem", "dynamodb:UpdateItem"]
      Effect   = "Allow"
      Resource = "arn:aws:dynamodb:us-east-1:123456789:table/users"
    }]
  })
}

If your application’s database user can DROP TABLE, and your application gets compromised, the attacker can DROP TABLE. If your application’s database user can only SELECT, INSERT, and UPDATE on specific tables, the blast radius shrinks dramatically.

The uncomfortable truth: most of us give our dev databases admin credentials because it’s easier. Then we use those same credentials in staging. Then production. And the line between “convenient for development” and “catastrophic in a breach” disappears entirely.


4. Errors Should Inform Developers, Not Attackers

Stack traces in API responses. Database error messages returned to the client. Verbose error pages in production. These are not just bad UX — they are reconnaissance gifts for anyone probing your system.

A stack trace tells an attacker what framework you’re using, what version, what the internal structure of your code looks like. A database error tells them your schema. A verbose 500 response tells them which query failed. Every piece of information narrows the search space.

// Insecure — raw error leaked to client
app.get("/user/:id", async (req, res) => {
  try {
    const user = await db.query(`SELECT * FROM users WHERE id = ${req.params.id}`);
    res.json(user);
  } catch (err) {
    res.status(500).json({ error: err.message, stack: err.stack }); // never do this
  }
});

// Secure — structured internal logging, generic client response
app.get("/user/:id", async (req, res) => {
  try {
    const user = await db.query("SELECT * FROM users WHERE id = ?", [req.params.id]);
    res.json(user);
  } catch (err) {
    logger.error({ err, userId: req.params.id }, "Failed to fetch user");
    res.status(500).json({ error: "Something went wrong. Please try again." });
  }
});

The rule: log everything internally, expose nothing externally. Use a structured logger (Pino, Winston, the standard library in your language of choice) that captures context — request ID, user ID, timestamp, error details — and send it somewhere you can query it. Return a generic, unhelpful message to the client. Unhelpful to attackers is exactly what you want.


5. Dependencies Are Attack Surface

You didn’t write most of your codebase. The average Node.js project has hundreds of transitive dependencies. Each one is code you are responsible for but did not author. Each one is a potential vector.

The Log4Shell vulnerability in 2021 affected millions of systems because a logging library had a critical flaw. The Equifax breach — which exposed 147 million people’s data — started with an unpatched Apache Struts vulnerability that had a known fix available for months. The patch existed. No one applied it.

# Check for known vulnerabilities in your dependencies
npm audit
# or
pip-audit
# or
dotnet list package --vulnerable

# Lock your dependency versions — don't use ranges in production
# package.json — use exact versions for critical dependencies
{
  "dependencies": {
    "express": "4.18.2",     // exact, not "^4.18.2"
    "jsonwebtoken": "9.0.0"  // exact, not ">=9.0.0"
  }
}

Run npm audit (or the equivalent for your stack) in CI. Subscribe to security advisories for the frameworks you depend on. Update dependencies on a schedule — not only when something breaks. And when a critical CVE drops, treat it as an incident, not a backlog item.

The Real Cost of Getting This Wrong

I want to be concrete about what we’re protecting against.

Nigeria’s Data Protection Act (NDPA) and the NDPC’s regulatory framework are not theoretical. Organisations that suffer data breaches face investigations, mandatory notifications, and potential fines. The CBN’s cybersecurity framework for financial institutions is explicit about the obligations of licensed entities. The NDPC has begun enforcement actions. This is no longer a “future concern.”

But beyond regulation, there is something more immediate: trust. Nigerian users have been burned before. They are cautious. When a fintech app leaks data, or an e-commerce platform gets breached, or a health platform exposes patient records — the damage is not just to one company. It erodes trust in the entire category. It sets the whole ecosystem back.

Every insecure line of code you ship is a small vote against the future of technology in this country.

The Work That Matters

I am not asking you to be perfect. I am asking you to be honest about the tradeoff when you skip input validation to hit a deadline. To name it clearly — “we are taking on security debt here” — and to treat it as debt: something with a balance, accruing interest, that will need to be repaid.

I am asking you to push back when a feature request arrives with no time for security review. To add npm audit to your CI pipeline. To never commit a .env file. To write the parameterised query even when the f-string is faster to type.

This Workers’ Day, let the work you are most proud of be the work that protected someone.

The security audit that caught the injection vulnerability before it shipped. The secrets rotation that happened before the credentials were ever exposed. The error handler that gave attackers nothing to work with. The dependency update that closed the CVE no one noticed.

That work does not get a demo. It does not get applause. But it is the work that earns the right to build more.

Do it properly. No shortcuts.

Obichukwu Onyeowuzoni

LinkedIn

20 years building enterprise systems across healthtech, fintech, and government. I write .NET, think in systems, and care deeply about software that works in the real world.

Full story →