09 Aug 2026 · 25 min read
The interview question about SSH keys
"You have a lot of hosts — how do you manage SSH keys?" is really a question about where authorisation state lives. Working the answer from authorized_keys through Vault SSH certificates to Session Manager, and admitting which layer actually solves the problem.
There is an interview question I have been asked and have asked, in roughly this shape:
You have a lot of hosts. How do you manage SSH keys?
It is a good question because it has no single right answer and a lot of wrong ones. It is also a question people usually answer at the wrong altitude — they name a tool. Ansible. A bastion. Teleport. SSM. Any of those can be correct, and none of them is an answer on its own, because the question is not really about SSH.
The question is: where does the authorisation state live, and what does it cost you to change it?
Everything below is a different answer to that one question, roughly in order of how well each one answers it. I have run some of these and not others, and I will say which is which.
Part A — On-premise, where you own the whole stack
1. Why plain keys collapse, and exactly where
Start with the naive setup, because it is genuinely fine at small scale and you should be able to say why.
Every engineer generates a keypair. Every server gets the public halves appended to
~/.ssh/authorized_keys for whichever accounts they need. Done. For five servers and
three engineers this is not a problem worth solving and building anything else would be
worse.
Now do the arithmetic. Fifty servers, twenty engineers. If everyone can reach everything,
that is 50 × 20 = 1,000 authorized_keys entries, spread across fifty files on fifty
machines, each of which is independently editable by anyone with root there. In practice
it is messier: multiple accounts per host (app, deploy, root), so the real number is
some multiple of that, and it is not uniform — access grew by request, one line at a time,
over two years.
The setup cost of this is nearly zero. That is the trap. The cost is entirely in the changes, and changes are the only thing that happens after week one:
- Someone joins. You need to know every host they should reach — which nobody has written down, because the source of truth is fifty files.
- Someone leaves. You need to find and delete their key on every host. Not most hosts. Every host. One missed line is a working credential held by a former employee, and there is no artefact anywhere that tells you it exists.
- Someone’s laptop is stolen. Same operation, same urgency, worse clock.
Offboarding is the whole problem. Onboarding fails loudly — the person cannot log in, they tell you, you fix it. Offboarding fails silently and permanently. Nothing in the system ever reports “there is a stale key on host 34”. You find out when someone audits, or when you do not find out at all.
And the failure is not that people are careless. It is that the state is distributed by design, so correctness requires an all-or-nothing operation across N independent machines, and you have no transaction.
2. Config management improves this, and does not fix it
The obvious next move is to stop editing files by hand. Ansible can own authorized_keys:
- name: Deploy engineer keys
ansible.posix.authorized_key:
user: deploy
key: "{{ lookup('file', 'keys/' + item + '.pub') }}"
state: present
loop: "{{ team_members }}"
This is a real improvement and worth doing. Git becomes the intended state, key changes go through review, and adding a person is a commit rather than fifty SSH sessions.
It does not solve the problem, for two reasons, and being able to name them is most of what this part of the interview is testing.
The exclusive flag is the whole game, and forgetting it is the default. The snippet
above adds keys. It removes nothing. A key deleted from team_members stays on every host
forever, because state: present on a shrinking loop is a no-op on the entries you dropped.
The correct version is exclusive ownership of the file:
- name: Deploy engineer keys — exclusive, so removals actually remove
ansible.posix.authorized_key:
user: deploy
exclusive: true # write the authoritative set, delete everything else
key: |
{% for m in team_members %}
{{ lookup('file', 'keys/' + m + '.pub') }}
{% endfor %}
exclusive: true means the module writes the authoritative set and deletes everything else
in the file — including the key someone added manually during an incident at 3am and never
mentioned. That is the point. It is also why people turn it off after it deletes something
once.
The state is still distributed, and the push is not atomic. This is the deeper problem and no flag fixes it. Revocation is still “reach every host and edit a file”. A host that was powered off, in maintenance, network-partitioned, or simply missing from the inventory does not get the change. It comes back up hours later still trusting the revoked key, and your playbook run reported success because that host was not in the run.
So the honest description of config-managed keys is: you have made the intended state legible and reviewable, and you have not changed the mechanism of enforcement at all. The question “is this key actually gone everywhere?” is still answered by walking every host and looking. That is better than nothing, and it is not an answer to the question.
3. The answer that fixes the root cause: an internal SSH CA
Everything above tries to manage the distribution of public keys. The fix is to delete that problem, which SSH has supported since OpenSSH 5.4 and most people have never used: certificates.
The model inverts:
- A CA keypair exists in exactly one place.
- Every host is told, once, at build time: trust this CA.
- A user proves their identity to something, gets their public key signed into a short-lived certificate, and presents the certificate.
- The host validates the signature locally. No network call, no lookup, no list.
On the host it is one directive, which OpenSSH documents as “a file containing public keys of certificate authorities that are trusted to sign user certificates for authentication”:
# /etc/ssh/sshd_config
TrustedUserCAKeys /etc/ssh/trusted-user-ca-keys.pem
AuthorizedPrincipalsFile /etc/ssh/auth_principals/%u
That is the entire per-host configuration, for every host, forever. It goes in the golden image and you never touch it again. Notice what is not there: any individual’s public key. The host has no idea who is in the team, and does not need to.
Vault as the CA
HashiCorp Vault’s SSH secrets engine is the low-cost way to run this. Setup is four commands (HashiCorp’s signed-certificates docs):
vault secrets enable -path=ssh-client-signer ssh
vault write ssh-client-signer/config/ca generate_signing_key=true
# The public half — this is what goes on every host, once.
vault read -field=public_key ssh-client-signer/config/ca \
> /etc/ssh/trusted-user-ca-keys.pem
vault write ssh-client-signer/roles/ops \
key_type=ca \
allow_user_certificates=true \
allowed_users="ubuntu,deploy" \
default_extensions='{"permit-pty":""}' \
ttl=30m0s
And issuing:
vault write -field=signed_key ssh-client-signer/sign/ops \
public_key=@$HOME/.ssh/id_ed25519.pub > ~/.ssh/id_ed25519-cert.pub
ssh -i ~/.ssh/id_ed25519 user@host
Four details in that role definition carry the design.
ttl=30m0s. The certificate expires in thirty minutes, and OpenSSH enforces that
locally — the validity window is a signed field in the certificate, checked by the target
host with no help from anybody. This is what makes the whole model work operationally, and
I will come back to it.
allowed_users is authorisation, not decoration. Vault refuses to sign a certificate
for a principal outside that list. The bound between “who can ask” and “what they can ask
for” lives in the Vault role, in one place, in code.
default_extensions is a deny-by-default surface. A certificate with no extensions
gets no PTY, no port forwarding, no agent forwarding, no X11. You add back only what the
role needs. {"permit-pty":""} and nothing else is a perfectly reasonable production role
— it grants an interactive shell and forbids using that session to tunnel anywhere.
Principals are where the authorisation model actually lives. A certificate carries a
list of principals — names, not usernames-in-a-vacuum — and the host decides which
principals it accepts. With AuthorizedPrincipalsFile /etc/ssh/auth_principals/%u, a
database host might contain:
# /etc/ssh/auth_principals/root
sre-oncall
# /etc/ssh/auth_principals/deploy
sre-oncall
platform-team
Now sre-oncall in a certificate gets you root here. platform-team does not — it only
gets deploy. The same certificate presented to an app server gets whatever that host’s
principal files say. Group membership travels in the certificate; the host-to-group mapping
travels with the host, in configuration management, reviewable. Neither of them is a list
of humans.
OpenSSH also has AuthorizedPrincipalsCommand if you want that mapping computed at login
time rather than written to disk, though that reintroduces a runtime dependency on
whatever answers it, which is exactly the thing certificates were buying you freedom from.
The two modes, and why signed certificates win for self-hosted
Vault’s SSH engine offers two working modes (key_type=ca and key_type=otp; the old
dynamic-keys mode was removed in Vault 1.13, so ignore anything on the internet that
recommends it):
Signed certificates (ca) | One-time passwords (otp) | |
|---|---|---|
| On the target host | one sshd_config directive | vault-ssh-helper binary, wired into PAM |
| Validation | local, offline, by OpenSSH itself | helper calls Vault on every login |
| If Vault is down | existing certificates still work until they expire | nobody can log in anywhere |
| Rollout to legacy hosts | config-management line | install and configure software per host |
For self-hosted infrastructure the certificate mode is the obvious pick, and the reason is the third row. OTP mode makes Vault a hard runtime dependency of logging into your servers — which is the thing you most need to do when Vault is having a bad day. Signed certificates degrade in the right direction: an outage stops you issuing new access, it does not lock you out of a machine you already have a valid certificate for.
The fourth row is the practical one. “One line in sshd_config” and “a binary plus PAM
configuration on every host” are not comparable rollouts, and the second one has
reintroduced the per-host state you were trying to eliminate.
What revocation means now
Here is the part that actually answers the interview question, and it is worth being precise rather than triumphant.
With authorized_keys, revocation is a write to N machines. It is O(hosts), it is not
atomic, it fails silently on unreachable hosts, and there is no way to verify it short of
checking all of them.
With short-lived certificates, revocation is mostly a write to one machine: you remove the person from the identity provider, or the Vault role, and they stop being able to get a new certificate. Their current one keeps working until it expires. With a 30-minute TTL, your worst-case exposure is 30 minutes, bounded, known, and stated in the role definition.
That is a fundamentally cheaper operation, and the cheapness is the security property — because an expensive revocation is one people delay, batch, or half-finish.
Two honest caveats.
You are trading a distribution problem for a TTL you have to justify. “Access revoked
in under 30 minutes” is a policy statement. If your compliance regime, or your own
judgement about a hostile departure, says minutes-not-half-hours, shorten the TTL and
accept the friction of more frequent signing. If you need immediate, OpenSSH’s
RevokedKeys directive takes a key revocation list, and now you are distributing a KRL to
every host — which is the original problem again, at lower volume. It exists as an
emergency lever. Do not build the routine path on it.
Protect the CA private key like it is the whole thing, because it is. One key now
grants root everywhere. Vault’s own storage, an HSM, or at minimum an unsealed-by-quorum
Vault with audit logging on the signing path. The single point of failure is real; it is
just a single point you can actually watch, which fifty authorized_keys files were not.
The lab: vssh
I built this at home rather than reading about it, as vssh: Vault in HA mode on Raft storage, Keycloak as the OIDC identity source, and the SSH secrets engine signing certificates. Keycloak is doing the load-bearing work in that sentence — the point of wiring OIDC to Vault is that group membership in the IdP becomes the thing that decides which Vault role you can reach, so offboarding is a single change in one directory and everything downstream follows.
One thing I hit that is worth passing on: the vault ssh CLI helper did not pass through a
port flag for my setup, so a host on a non-standard SSH port could not be reached through
it. The fix was not clever — a vssh wrapper script that does the signing call and then
invokes ssh itself with the arguments I actually wanted. That is the general shape of
adopting this: the certificate model is sound and the convenience wrapper around it is
where you will spend an afternoon.
Honest assessment:
- Effectiveness: highest of anything in Part A. It is the only option that removes the distribution problem rather than automating it.
- Setup effort: not trivial. Vault HA, Raft, unseal strategy, an IdP integration, and a CA key whose compromise is catastrophic. This is a project, not an afternoon.
- Licence cost: zero. Vault’s SSH secrets engine and Keycloak are both open source. You pay in operations, not in licences.
4. The bastion, and what it is actually for
A bastion host comes up in every answer to this question, usually first. It belongs here, after the CA, and I want to be blunt about why.
A bastion does not solve key management. If your engineers hold long-lived keys, putting
a jump host in front of the fleet means they hold long-lived keys and there is now a
machine in the middle. The authorized_keys files did not go away. You added one more of
them, on the most exposed host you own.
What a bastion genuinely does:
- Narrows the network entry point. Port 22 is reachable on exactly one address, so everything else can be closed at the security group or firewall. That is real, and it is the main reason to have one.
- Centralises audit and network policy. One place to log connections, one place to rate-limit, one place to alert on.
Those are network-layer wins. They are worth having. They are simply orthogonal to the question of who holds what credential — which is why the standard on-prem shape is bastion and CA: the bastion decides what is reachable, the CA decides who you are. They compose cleanly and neither substitutes for the other.
Two configuration points that matter more than the architecture diagram.
Use ProxyJump, not agent forwarding. Agent forwarding (ssh -A) forwards your
authentication agent to the bastion, and anyone with root on the bastion can use that
socket to authenticate as you to everything your key opens, for as long as your session
lasts. That is not a theoretical risk; it is the documented reason ProxyJump exists.
ProxyJump (OpenSSH 7.3, August
2016) uses the bastion as a TCP forwarder only —
your client authenticates end-to-end with the target and the bastion never sees your agent:
# ~/.ssh/config
Host bastion
HostName bastion.example.internal
User jump
Host 10.0.*.*
ProxyJump bastion
# No ForwardAgent. The bastion is a pipe, not a principal.
The bastion should not be special. It runs the same TrustedUserCAKeys line as
everything else and holds no long-lived keys of its own. A bastion that is the one host
with hand-managed authorized_keys is the weakest machine in the estate wearing a
security-shaped label.
5. Enterprise platforms — named, not rated
Above a certain size the answer stops being “assemble it” and becomes “buy it”: Teleport, HashiCorp Boundary, StrongDM. Broadly they wrap the same certificate-and-identity machinery in a product with a central UI, session recording, per-session approval workflows, and the compliance reporting that an auditor will ask for by name.
I have not run any of them in production. I am not going to compare their feature matrices here, because a comparison I cannot back with operational experience is worth less than no comparison at all — it just launders a vendor page through a blog post. If you are at the scale where this is the question, read the vendors directly: Teleport, Boundary, StrongDM.
The one thing I will say, from the outside: the value proposition is almost never the SSH
part. The SSH part is TrustedUserCAKeys and you can have it for free. What you are buying
is the session recording, the approval workflow, the single console across SSH and
databases and Kubernetes, and someone to call. Those are real things to want. Just be clear
that is what the invoice is for.
6. Part A, side by side
| Setup effort | Ongoing effort | Licence cost | Solves key distribution? | |
|---|---|---|---|---|
Plain authorized_keys | none | grows with hosts × people | none | No — state is on every host |
| Ansible-managed keys | low | moderate; every change is a fleet-wide push | none | No — automates the same distribution |
| Bastion alone | low | low | none | No — different layer entirely |
| SSH CA (Vault) | high — Vault HA, IdP, CA custody | low — issue, don’t distribute | none (OSS) | Yes — hosts trust a CA, not a list |
| Bastion + SSH CA | high | low | none (OSS) | Yes, plus a narrowed network edge |
| Enterprise platform | moderate — mostly integration | low | significant | Yes, and a lot you did not ask for |
The row worth staring at is Ansible-managed keys: low setup, moderate forever, and a “No” in the column that matters. That is the classic shape of a solution that automates a problem instead of removing it, and it is comfortable enough that teams stay there for years.
Part B — The same question on AWS
7. Why the problem changes shape
Move that fleet to EC2 and something is different, and it is not the SSH protocol.
The identity layer already exists. On-prem, the hardest part of the CA design was not the CA — it was standing up and integrating an identity provider so that “who are you” had an answer worth signing. On AWS, IAM is that, it is already there, every engineer already authenticates through it, and it is already the thing that gates everything else you do.
So the question changes from “how do I build a system that issues short-lived credentials against a trusted identity?” to “how do I connect the SSH problem to the identity system I am already running?” — and AWS ships two answers to that, both of which you operate by writing an IAM policy.
This is the step where you get the same outcome for less operational surface. You can absolutely run Vault on EC2 and sign certificates exactly as above, and if you have a hybrid estate with on-prem hosts that need the same access model, that is a good reason to. But if the fleet is AWS-native, building a CA to solve a problem AWS already solved is work you chose.
8. EC2 Instance Connect, and the Endpoint
EC2 Instance Connect (EIC) is an ephemeral-key mechanism. You call an API, AWS pushes your public key to the instance’s metadata, and it is removed after 60 seconds — you have that window to complete the SSH handshake, after which the key is gone. The session itself persists normally once established.
The IAM-gated part is the push:
aws ec2-instance-connect send-ssh-public-key \
--region us-west-2 \
--availability-zone us-west-2b \
--instance-id i-001234a4bf70dec41EXAMPLE \
--instance-os-user ec2-user \
--ssh-public-key file://my_key.pub
ssh -o "IdentitiesOnly=yes" -i my_key ec2-user@10.0.1.23
The authorisation decision is ec2-instance-connect:SendSSHPublicKey, and it can be scoped
by instance and by OS user — so “this role may push keys for deploy on instances tagged
env=staging” is an IAM policy, not a fleet of files. Every attempt, successful or not,
lands in CloudTrail.
Requirements, because this is where people get it wrong:
- The instance needs the EC2 Instance Connect package installed (preinstalled on recent Amazon Linux and Ubuntu AMIs, installable elsewhere).
- Port 22 is still open, and something must be able to reach it. Plain EIC does not
give you network reachability — it gives you a credential. Using the console requires the
instance to have a public IPv4 or IPv6 address, and the inbound rule should source from
the AWS-managed prefix list
com.amazonaws.<region>.ec2-instance-connectrather than0.0.0.0/0.
That reachability gap is what EC2 Instance Connect Endpoint (EICE) closes. AWS describes it as “an identity-aware TCP proxy”: you create an endpoint in a subnet, AWS provisions an ENI there, and your client tunnels to instances by private IP — no public IP, no internet gateway, no bastion.
aws ec2-instance-connect ssh \
--instance-id i-001234a4bf70dec41EXAMPLE \
--connection-type eice
Worth knowing before you design around it (AWS docs, checked August 2026 — quotas move):
- One endpoint per VPC and per subnet. Adding one in a different AZ of the same VPC means deleting the existing one first.
- 20 concurrent connections per endpoint.
- Maximum TCP connection duration of one hour (3,600 seconds), and you can cap it lower
in the IAM policy via the
OpenTunnelpermission. - It is for management traffic. High-volume transfers are throttled. Do not build a data pipeline through it.
- Client IP preservation is off by default, is IPv4-only, and does not work through a transit gateway. With it off, instance security groups must allow traffic from the VPC rather than from the client.
On cost: AWS states plainly that there is no additional cost for EC2 Instance Connect Endpoints, with the caveat that reaching an instance in a different Availability Zone from the endpoint incurs normal cross-AZ data transfer charges (AWS documentation, checked August 2026; pricing changes, verify before you budget). Combined with the one-endpoint-per-subnet quota, that is a mild architectural nudge toward an endpoint per AZ you actually use.
So: EIC is the credential story, EICE is the reachability story, and the pair together replace both the key distribution problem and the bastion, using IAM as the CA you did not have to build.
9. SSM Session Manager — no key material at all
Session Manager comes at the same problem from the other end. There is no SSH key, no certificate, and no inbound port.
The mechanism is that the SSM Agent dials out. It holds an outbound HTTPS connection to the Systems Manager service, and a session is established over that channel — so the instance needs no inbound reachability whatsoever. AWS’s own description: “secure node management without the need to open inbound ports, maintain bastion hosts, or manage SSH keys.”
aws ssm start-session --target i-001234a4bf70dec41EXAMPLE
Port 22 can be closed. Not restricted — closed. Delete the rule.
What it needs:
- SSM Agent on the instance (preinstalled on Amazon Linux, recent Ubuntu, Windows Server AMIs).
- An IAM instance profile granting the agent permission to talk to Systems Manager —
AmazonSSMManagedInstanceCoreis the managed policy, and this is the single most common reason an instance does not show up as a managed node. - Outbound 443 to
ssm.<region>.amazonaws.com,ssmmessages.<region>.amazonaws.comandec2messages.<region>.amazonaws.com— via NAT, or via VPC endpoints. - The Session Manager plugin on the operator’s machine for CLI use.
The authorisation is ssm:StartSession scoped by instance, which composes with tags the
same way any IAM policy does. And session activity can be logged to S3 or CloudWatch
Logs, keystrokes and output, with optional KMS encryption — which is the audit story
enterprise platforms charge for, available here as a checkbox.
Being honest about the cost
Session Manager on EC2 instances is listed at no additional charge (AWS Systems Manager pricing, checked August 2026). One current-as-of-now detail worth flagging because it is recent: for hybrid and multicloud nodes — on-prem servers and other people’s clouds registered as managed nodes — Session Manager moves to $0.05 per session from 30 September 2026, following the removal of the Advanced Instances Tier on 30 June 2026. EC2 is unaffected. If your answer to this interview question involves managing on-prem hosts through SSM, that is a number that changed this year and will change again; check the pricing page rather than this post.
The other place cost hides is the private-networking path. If your instances have no
internet route, the recommended setup is interface VPC endpoints, and those are not
free. For Session Manager specifically AWS lists ssm, ssmmessages and ec2messages as
the relevant interface endpoints (plus an S3 gateway endpoint for agent updates, and
optionally kms and logs if you encrypt or log sessions). PrivateLink’s billing model:
you are “billed for each hour that your VPC endpoint remains provisioned in each
Availability Zone”, plus data processing charged at $0.01/GB for the first 1 PB per
month, then $0.006/GB, then $0.004/GB (AWS PrivateLink pricing, checked August 2026).
I am deliberately not quoting a single monthly figure, because the hourly rate is per-region and the multiplier is what actually bites: three endpoints × the number of AZs you place them in, billed continuously whether anyone opens a session or not. That is the shape to reason about. Three endpoints across two AZs is six billable endpoint-hours every hour, forever, to avoid a NAT gateway that is also billed hourly. Do that arithmetic with current numbers for your region before calling SSM “free” — the service is free, the private path to it is not.
For a small estate that already has a NAT gateway, the honest answer is that Session Manager costs you nothing extra. For a small estate with no egress at all, VPC endpoints may cost more than the instances.
10. Choosing between them
They are not competitors so much as different shapes, and the decision has a fairly clean seam.
| EC2 Instance Connect (+ Endpoint) | SSM Session Manager | |
|---|---|---|
| Credential | ephemeral SSH key, 60s validity | none |
| Inbound port 22 | required (from the endpoint or prefix list) | not required — close it |
| Reachability | direct, or tunnelled via EICE | outbound agent connection only |
| On-instance requirement | EC2 Instance Connect package | SSM Agent + instance profile |
Native ssh, scp, rsync, ProxyJump | yes — it is real SSH | via port forwarding, with caveats |
| Session recording to S3/CloudWatch | no (CloudTrail records the key push) | yes |
| Non-EC2 / on-prem nodes | no | yes (and priced separately from Sept 2026) |
| Where cost appears | cross-AZ data transfer | VPC endpoints, if you need a private path |
Pick EIC/EICE when SSH semantics matter. If your tooling is scp, rsync,
ansible_connection=ssh, an IDE’s remote-development mode, or anything that expects a real
SSH transport, EIC keeps all of it working unchanged while removing the long-lived key. You
are still doing SSH; you have just made the credential ephemeral and the authorisation an
IAM policy.
Pick SSM when the absence of key material and the presence of an audit trail matter more. No credential to steal, no port to scan, and a recording of the session in S3. For a compliance-shaped requirement — “produce evidence of who accessed production and what they typed” — this is the answer, and it is the built-in one.
One caveat that belongs in any honest comparison, straight from AWS’s documentation: session logging is not available for sessions that connect through port forwarding or SSH. Because SSH-over-Session-Manager is end-to-end encrypted between your client and the instance, Session Manager is only a tunnel and cannot see the contents. So the common workaround of “run SSH over SSM to keep my tooling” quietly gives up the recording that was the reason to choose SSM. You get one or the other on a given session. Know which one you picked.
In practice a lot of AWS estates run both: SSM as the default interactive path with session logging on, EIC/EICE available for the tooling that genuinely needs SSH transport. Both are IAM policies. Neither is a key on a laptop.
11. A third-party precedent: Netflix BLESS
Worth studying, and worth being accurate about its status.
BLESS — Bastion’s Lambda Ephemeral SSH Service — is an SSH certificate authority that runs as an AWS Lambda function. The CA private key is encrypted with KMS; invoking the Lambda is gated by IAM; the function returns a short-lived signed certificate; hosts are configured to trust the CA and hold no user keys. It is Part A’s design with the CA replaced by a function invocation, and it was a genuinely interesting idea in 2016 — the CA has no server, the key is at rest in KMS, and “who may request a certificate” is an IAM policy, years before AWS shipped anything like Instance Connect.
It is now archived. The repository carries this notice:
With the existence of more SSH certificate tools since the release of BLESS, and better SSH access management from AWS, we’re moving BLESS to the archived OSS project state. This means we no longer plan to maintain the project, but will be keeping it public for others who may still use it.
That is Netflix’s own framing and it is the correct one: do not deploy this today. It matters as a design precedent — an ephemeral CA as a function, with key custody delegated to KMS and authorisation delegated to IAM — and the reason it stopped mattering is exactly the argument in section 7. AWS built the identity-gated ephemeral access path into the platform, and once that happened, running your own Lambda CA became work you chose.
There is a lesson in the archival notice that generalises past SSH: a good piece of infrastructure you build to fill a platform gap has a shelf life ending on the day the platform fills the gap. That is not a failure. It is the expected outcome, and it is a reason to keep such things small.
12. The whole thing, in one table
| Approach | Effectiveness | Effort to build | Effort to run | Cost |
|---|---|---|---|---|
Plain authorized_keys | low — unbounded, unverifiable state | none | high and growing | free until an audit |
| Config-managed keys | low-moderate — legible, still distributed | low | moderate forever | free |
| Bastion | orthogonal — network, not identity | low | low | one instance |
| SSH CA (Vault + OIDC) | high — removes distribution entirely | high | low | free (OSS), paid in ops |
| EC2 Instance Connect + Endpoint | high, on AWS — IAM is the CA | low | very low | endpoint free; cross-AZ transfer |
| SSM Session Manager | high, on AWS — no key material at all | low | very low | free on EC2; VPC endpoints if private |
| Teleport / Boundary / StrongDM | high, plus audit and workflow | moderate | low | licensed |
Read down the “effectiveness” column and then down “effort to run”. The two AWS rows are the interesting ones: they reach the same place as the CA, and the operational column is near-empty, because someone else is running the CA. That is the whole argument for the cloud-native option, and it only holds while your fleet actually lives there.
13. So what is the answer?
The answer to the interview question is not a tool. It is: match the mechanism to the scale and to the actual risk, and be able to say what the revocation path costs.
- Under ten hosts, one or two people. Plain keys, honestly. Write down where they are. Building anything else is the more expensive mistake.
- Tens of hosts, a team, on-prem. Config-managed keys is the plateau most teams stop at, and it is where the silent-offboarding failure lives. Certificates from an internal CA is the step that actually changes the mechanism. Add a bastion for the network edge — as well as, never instead of.
- On AWS. Start with Session Manager, because it is the smallest thing that closes port 22 entirely and gives you a session log. Add EIC/EICE where SSH transport is genuinely needed. Do not build a CA to solve a problem your platform already solved — unless you are hybrid, in which case one model across both is worth the Vault.
- Large, regulated, audited. Buy the platform. Not for SSH — for the recording, the approvals and the console.
The failure mode I would flag hardest is the one that looks like diligence. Spending a quarter standing up Vault HA, an OIDC integration and CA key custody to manage six hosts is not a security win. It is a large new system with its own failure modes, its own upgrades and its own single point of catastrophic compromise, protecting an estate that fits in a text file — and the person who built it has spent a quarter of engineering time buying an improvement nobody could measure.
Under-engineering this leaves stale keys on hosts. Over-engineering it produces a system too complex for the team to keep running, which decays, and then leaves stale keys on hosts. The interesting part of the question was never which tool. It was whether you can tell which of those two you are about to do.