Access is a liability, not a convenience
The meeting was going well until the last ten minutes. We had walked a Nairobi financial services group through the whole ERPNext build, the reporting they had asked for, the migration plan for their old system. Then I said the sentence I had said a hundred times before without thinking about it: "we will just need SSH access to the dev server."
The room changed. Not dramatically, and nobody said no. But the head of IT wrote something down, the procurement lead asked who else in my company would have that access, and a meeting that had been about software became a meeting about risk. Three weeks of back and forth followed. In the end we did not get access, and their team ended up pulling and migrating by hand every time we shipped a change, which is exactly the arrangement nobody wanted.
I got that wrong, and not in the way I initially thought. My mistake was not phrasing. It was that I had asked for something I did not want to be given. If you build systems for other people's businesses, the credential you are holding is not a convenience you have earned. It is a liability you have accepted, and there is a much better answer available: deploying Frappe apps to a client server you are not allowed to touch, using a mechanism where the credential never exists in the first place.
an SSH key on a client's production server is a permanent, transferable grant of everything that server can reach, held by you, defended by you, and explained by you after any incident.
- The key does not grant "deployment". It grants a shell, and a shell grants the database, the files and every customer record in them.
- Once it exists it can be copied, and a copy leaves no trace. Rotating it needs the client's cooperation anyway.
- "Give me access" moves a software conversation into a procurement risk conversation, and you will lose weeks there.
- A deployment story with no inbound access and no vendor-held credential is a thing you can sell. It answers a security questionnaire before it is written.
What one SSH key actually grants
We talk about SSH access as though it is scoped to the task we intend to use it for. It is not scoped to anything. An authorised key on a server is an authentication mechanism, and authentication is not authorisation. What it grants is whatever the account it logs into can do, which on a Frappe server is a great deal more than deployment.
None of this is an argument that SSH is badly designed, and hardening it properly is worth doing on any box you run - locking down a fresh VPS in twenty minutes covers that. The point is narrower: hardening changes how hard the key is to misuse, not what it grants once used.
On a standard Frappe install the bench user owns the apps, the sites directory and the Python environment. The database password for the site sits in plain text in that site's configuration file, which is exactly how the framework is designed to work and exactly why a shell as that user is a shell into the database. Anyone who reaches the shell reaches the customer records, without ever running an exploit. They just read a file and connect.
So when a client's security lead hesitates at "just SSH access", they are not being obstructive and they are not confused about what you intend to do with it. They are reading the grant correctly and you are not.
Why the request lands so badly in procurement
There is a specific reason this sentence causes trouble in an enterprise conversation, and it has nothing to do with trust in you personally.
The moment a vendor holds a credential to a production system, that vendor enters the client's risk register. Somebody now has to answer questions in writing: who at the vendor holds it, what happens when that person leaves, is it on a personal laptop, is that laptop encrypted, is there an offboarding process, does the vendor carry cyber liability cover, what is the notification obligation if the vendor is breached. In a regulated environment - and in Kenya that increasingly means anyone touching customer data under the Data Protection Act - the client also has to treat you as a data processor and paper the relationship accordingly.
None of those questions are unreasonable. All of them take weeks. And the honest thing to notice is that every one of them exists only because of a request you made for your own convenience. You created the risk item. You are now the reason the project is in a procurement queue.
The alternative is not "refuse to deploy". It is to arrive with an architecture where the question never comes up, because there is nothing to register.
Blast radius, both ways
Let me be fair to the model I am arguing against, because the honest comparison is not "SSH is dangerous, runners are safe". Both models have a failure mode. They are just different failures with very different shapes.
The runner model is not free of risk, and I will not pretend otherwise. If your client's server runs an agent that executes whatever your repository tells it to, then push access to your repository is command execution on their machine. That is a real and serious property, it deserves its own article, and it is the subject of hardening a self-hosted runner against the push-access problem.
But notice the difference in shape. The SSH risk is a bearer credential: it works for whoever holds it, from anywhere, with no approval step, and its theft is invisible. The runner risk is an execution path you can put gates on - branch protection, tag protection, an approval the client themselves grants. One risk is a fact about a file. The other is a workflow you control and can constrain.
The commercial argument, which is the real one
I have made the security case because it is true, but it is not why I changed how I work. I changed because of what happened in the next procurement conversation after that one.
Different client, similar size, same stage in the process. When we got to deployment I said: your server will connect out to our release system, pull the tagged release, and run the upgrade. Nothing connects in. We hold no credential to your environment. Your IT team installs the agent, and can stop it at any time without telling us. Would you like the exact commands now or with the contract?
That took ninety seconds and it did not go to a risk register. It also did something I did not expect: it changed how they saw the rest of the proposal. A vendor who has clearly thought about the shape of their own access reads as a vendor who has thought about other things too.
This is worth being blunt about, because a lot of independent developers and small firms across East Africa are competing for exactly these accounts against much larger integrators. You will not out-resource them. You can absolutely out-architect them on this specific point, because the large integrator will usually turn up asking for a jump host, a service account and a VPN profile, and you will turn up asking for nothing.
What it costs you
Every recommendation has a bill, and this one has three lines on it.
You lose the midnight fix. When something breaks in production you cannot open a terminal and poke at it. You will diagnose from logs the client sends you, or from a screen share, and it will be slower. The first time this happens you will want the key back. Do not ask for it. Build better diagnostics instead, because the second time it happens the diagnostics will pay for themselves.
Setup moves earlier and involves someone else. Somebody on the client's side has to run an install once, and you have to write instructions good enough for a person you have never met to follow without you watching. That is real work, and it is work you do before you have been paid.
You have to actually have a pipeline. Deploying by hand over SSH tolerates a mess of undocumented steps living in your head. A pull-based or runner-based deploy does not. Every step must be written down in a script that runs unattended, which means you will discover, in week one, three things you have been doing manually for years without noticing. That discovery is unpleasant and it is the single most valuable part of the exercise.
The trade is: slower to fix, faster to ship, and no longer the answer to the question "who else could have touched this data".
Where this goes next
The rest of this argument is mechanical rather than philosophical. If the client's server is going to initiate everything, something on it has to do the initiating, and the shape of that thing determines everything else. That component is a runner, and understanding what a runner actually is, and why a self-hosted one needs no inbound connection is the single idea that makes the whole approach work.
If you want the finished implementation rather than the reasoning - the workflow file, the deploy script, the checklist you hand to the client's IT team and the security conversation to have before they raise it themselves - that is all in the full walkthrough of deploying to a client server you cannot touch.
And if you own the box yourself, none of this applies to you. Use SSH. It is simpler, it is well understood, and the SSH deployment path for Frappe is written up honestly, including what it costs. The argument in this article is about servers that belong to someone else.
Frequently asked questions
- The client offered me full root access. Should I take it?
- No. Take the narrowest access that lets you do the work, and prefer none at all for production. Root on a client's server means every future incident on that box starts with a question about you, and you will be answering it without logs of your own. If you need root for the initial install, take it for that window, hand it back, and put the change in writing.
- Is refusing server access not just making my job harder?
- It makes the first week harder and every week after that easier. The work moves from ad-hoc SSH sessions to a repeatable pipeline, which you needed anyway the moment a second person joined the project. What you lose is the ability to fix things by hand at midnight without telling anyone, and that was never an asset.
- What do I say when procurement asks how I deploy without access?
- That the client's server initiates every connection outward, that no inbound port is opened for you, that no credential to their environment exists on your side, and that they can stop the whole mechanism themselves with one command. Those four sentences answer most of a vendor security questionnaire before it is asked.
- Does this apply to a small business client too?
- The security argument matters less, but the liability argument is identical and the commercial argument is stronger. A small business owner who loses data will look for someone to blame, and a vendor holding the only SSH key is the obvious candidate. Not holding the key is cheap insurance regardless of client size.