When you put several projects and multiple teams on a single Linux VPS, access management matters alongside CPU and RAM capacity. Who can log in where? Who can deploy? Who can touch production databases? If you do not design a clear user, group and sudo structure from day one, the server slowly turns into a black box of shared passwords, random symlinks and mysterious file owners.
This guide connects Linux users, groups and sudo permissions in one worked multi-project model. It retains separate production/staging roles, deployment and runtime identities, file/ACL controls, service scope, database privileges, SSH and offboarding. The concrete shop exercise below is reusable for other projects; a role name alone is not an access boundary.
Why User, Group and sudo Design Matters on a Multi‑Project VPS
On a single‑tenant server, it is tempting to use one account such as ubuntu or root, share its SSH key in the team and move on. On a multi‑project VPS used by several developers, agencies or clients, that approach quickly becomes a liability.
- Weak accountability: With shared accounts, account-level logs alone cannot reliably answer questions like “Who edited
config.phpyesterday?” or “Who ran that dangerous command?” - Hard permission boundaries: When all code runs under one user, it is hard to keep Project A from reading or overwriting Project B’s files.
- Risky operations: If everyone has full
sudo, a typo in production can drop a database or bring the web stack down. - Painful off‑boarding: When someone leaves, shared keys and passwords they knew must be rotated; individually assigned keys can instead be revoked.
A clear architecture for Linux users, groups and sudo helps address these problems. It supports:
- Per‑person access: One Unix account per human, their own SSH key, clearer individual revocation.
- Per‑project access boundaries: Dedicated project groups and restrictive directory permissions limit file access. Separate runtime users are also needed to keep applications from sharing the same file-access identity.
- Role‑based sudo: Carefully scoped rules can allow developers to restart PHP‑FPM or Nginx without granting full administration. The boundary also depends on preventing them from modifying code or configuration that those commands execute as root.
- Cleaner deployments: CI pipelines and automation tools use well‑defined service users with minimal permissions.
A developer who can change deployed code can also influence what that runtime account does with its application data. Separating runtime from root limits this scope; it does not make an untrusted code author unable to reach the application’s own secrets.
If you are setting up a brand‑new VPS at dchost.com, this should sit right next to your first‑day tasks like updates and firewall rules. Our guide on the first 24 hours on a new VPS (updates, firewall and users) pairs nicely with the concepts we will cover here.
Core Building Blocks: Users, Groups, Permissions and sudo
Before designing the architecture, it is worth revisiting the basic Linux concepts we are going to combine.
Users: Human vs service accounts
A user in Linux is an identity: it owns files and can run processes. Separate these three purposes on a multi-project VPS:
- Humans: personal accounts such as alice, bob and carol, enrolled with the intended SSH and sudo authentication policy.
- Deployment accounts: own releases and run only approved tasks; avoid running builds or migrations as root.
- Runtime accounts: separate process identities per project/environment. A nologin shell denies normal shell execution; systemd can still start a program under that identity.
A group is a way to give several users the same access to some resources. On a multi‑project VPS, groups are your main tool for representing:
- Projects:
proj_blog,proj_api - Teams:
team_frontend,team_ops - Roles:
sudo_webfor web stack operations,sudo_dbfor DB operations
Permissions are then applied to these groups at the directory and sudo level.
Permissions and umask
Linux file permissions define what the owner, group and others can do: read (r), write (w), execute (x). For example, 750 means:
- Owner:
rwx(7) - Group:
r-x(5) - Others:
---(0)
We will rely on group permissions heavily, so it is worth revisiting our article explaining Linux file permissions (644, 755, 777) for shared hosting and VPS if you need a refresher.
The umask controls default permissions of new files. For collaborative project directories, we usually want new files to be writable by the group, not just the owner.
sudo: Controlled privilege escalation
sudo lets a user run commands as another user (often root) and normally logs command invocations. That does not by itself provide a complete audit of actions inside an invoked shell or application. The configuration lives in /etc/sudoers and included files; verify that /etc/sudoers.d/ is actually included. With sudo you can say things like:
- A project operations group can receive exact commands for a dedicated project unit. Shared nginx/PHP-FPM restarts remain shared maintenance.
- Database tasks use a restricted OS identity plus appropriate database privileges, not unrestricted root clients.
- Other users receive no sudo from these rules; inspect every additional rule and alternative authorization path.
Scoped sudo rules make the permitted target identity and operation explicit. Review their dependencies and test both permitted and rejected commands.
Designing an Architecture for Multiple Projects and Teams
Plan a model for several projects and teams, then implement and test one project at a time. The corporate, API and shop names below illustrate inventory; the complete executable recipe uses shop so its accounts, paths, ACLs and sudo rules can be followed without hidden prerequisites.
Step 1: Map your reality
In a quick planning session, list the things you need to represent on the server:
- Projects: For example
corporate‑site,api‑backend,shop‑frontend. - Environments: Production, staging, sometimes development.
- Teams and roles: Who deploys? Who only reads logs? Who manages databases?
- External parties: Any agencies or freelancers that should only touch one project?
Use this inventory to define the actual accounts, file permissions and sudo rules. Test the resulting grants against the access each person needs.
Step 2: Define user types
We recommend three main types of accounts on the VPS:
- Personal users: one per person, with individual keys and traceable task grants.
- Deployment identities: one per project/environment; own published code and perform release tasks.
- Runtime identities: one per relevant isolation boundary, without write permission to their code. Shared package users such as nginx do not create project isolation by themselves.
Separate personal and deployment users so code ownership can remain stable when a team member leaves. Offboarding still requires a review of owned files, active processes and credentials; release history and rollback need their own deployment controls.
Separate deployment identities for prod and stage as well: a single account owning both trees can cross the boundary even if their group names differ.
Step 3: Define group types
Next, define groups that mirror your projects and roles:
- Project groups: One per project per environment, e.g.
g_corp_prod,g_corp_stage,g_api_prod. - Role groups for sudo:
sudo_web(web stack operations),sudo_db(DB ops),sudo_ops(full operations). - Optional team groups: If you have large teams, you can add
team_front,team_backetc. Standard local Unix groups do not nest: add each user to the required project groups, manually or through automation. For most small VPS setups, project groups alone are enough.
In practice, a developer’s Unix account will end up looking like:
- Personal primary group, such as alice.
- Only the project read/write and operations groups explicitly needed; the worked example uses proj_shop, shop_read and shop_ops.
- No automatic nesting or intersection: being in both a project and a generic role does not create a new restricted policy.
Step 4: Plan directory layout
A predictable directory structure makes permissions much easier to reason about. A common and clean pattern is:
/srv/
corp-site/
prod/
app/ # code
shared/ # uploads, cache, logs
stage/
app/
shared/
api-backend/
prod/
app/
shared/
Give each project/environment its own ownership and access policy, and check traversal on every parent directory. Separating only the prod/stage leaf group is insufficient if a shared owner/runtime identity can reach both. Nginx site filenames help naming consistency, but do not enforce Unix or sudo access boundaries.
Step 5: Decide your sudo policy
Finally, write down who should be able to do what with elevated privileges. A minimal but practical set:
- Developers: edit/deploy only their approved project/environment using a restricted deployment identity.
- Project operations: exact service/log tasks for a dedicated unit; shared nginx/PHP-FPM/Supervisor operations affect every consumer.
- A separate, tightly controlled administrative role: full host management and recovery where justified. A project role must not quietly inherit it.
We will implement this with role‑based sudo groups in a moment.
Concrete Example: One VPS, Three Projects, Two Teams
Imagine a VPS at dchost.com hosting three applications for a small company. These are access-inventory examples; the later shop recipe demonstrates the permissions with a synthetic static page and does not install all three application stacks:
- Corporate website: WordPress at
corp.example.com - API backend: Laravel at
api.example.com - Shop frontend: Next.js at
shop.example.com
Two teams work on it: Web (WordPress + frontend) and Backend (API + some WordPress integration).
Users
Conceptually each project has personal, deployment and runtime identities. For the complete shop example, use the same five named accounts throughout:
alice: code/shared writer throughproj_shop, log reader throughshop_read.bob: onlyshop_read; reads code/logs, without access to shared data.carol: selected service operations throughshop_ops, reading throughshop_read; no automatic code-write permission.deploy_shop: code owner and target of a limited local maintenance task.svc_shop: separate runtime identity that can read code but cannot change it.
Groups
- Conceptual project/environment groups: g_corp_prod, g_corp_stage, g_api_prod, g_shop_prod; names are examples rather than created objects.
- Executable shop recipe: proj_shop for writers, shop_read for code/log readers and shop_ops for project operations.
- A global administrator role is separate; database access also requires actual database grants.
Directory ownership
We may decide:
- Code: a deployment account owns the app tree; normal files are not assigned executable/setgid bits recursively.
- Runtime: a separate account reads code and writes only the application’s necessary mutable paths.
- Readers/writers: actual permissions and ACLs implement the access matrix; a read-only role cannot cancel membership in a writer group.
Directory setgid gives newly created objects the directory group; it does not grant write access by itself. Regular files are not made executable recursively. The ACL grants code read/search access to svc_shop and shop_read, while proj_shop can write. The mask limits effective named-user/group and owning-group permissions. chmod g=... can change that mask; inspecting the group entry alone is insufficient.
With a default directory ACL, the inherited ACL and the application’s requested creation mode determine a new file’s permissions. An explicit 0600 mode can remove effective reader access. Copying, extraction, editor replacement/rename and deployment tools may alter or omit ACLs. The example explicitly restores the index file ACL after install for this reason. Verify effective access after each real deployment.
The POSIX ACL manual documents creation and access-check semantics.
Group membership
We might assign group membership like this:
alice: code/shared writer throughproj_shop, log reader throughshop_read.bob: onlyshop_read; reads code/logs, without access to shared data.carol: selected service operations throughshop_ops, reading throughshop_read; no automatic code-write permission.deploy_shop: code owner and target of a limited local maintenance task.svc_shop: separate runtime identity that can read code but cannot change it.
The code owner is deploy_shop, while svc_shop only receives named ACL access to code. Do not add the runtime identity to a code-writer group. Real CI credentials and artifact publication require a separate reviewed design; the local fixed task below is not a full deployment pipeline.
sudo rules
We then declare in /etc/sudoers.d/ (details in the next section):
- proj_shop: the fixed release-check task as deploy_shop, with no extra arguments.
- shop_ops: exact restart/log commands for shop.service, not arbitrary shared web-stack or database clients.
- A separate administrative group may intentionally have full host privilege; it is not created by this project exercise.
The resulting policy can be explained and tested, but its safety depends on all effective grants and on protected service/configuration paths. Naming groups or passing visudo does not itself establish an isolated or auditable deployment.
Implementing the Design Step‑by‑Step on Linux
Implement the shop example first, then repeat with distinct identities and paths for the corporate/API projects and their environments. The following prerequisites and commands are complete for the account/permission exercise, not for production application deployment.
1. Create Project Groups and Required Accounts
This worked example targets Ubuntu 24.04 with a POSIX shell, shadow user tools, sudo and the acl package. Start on a separate test server; do not apply it wholesale to existing accounts or a populated project tree. Run the setup commands as an administrator/root. Account names and /srv/shop must be new; the example stops on existing names. Enroll human SSH keys, the intended password/PAM policy and recovery access separately. Creating an account does not prove that login works.
#!/bin/sh
set -eu
for account in alice bob carol deploy_shop svc_shop; do
if getent passwd "$account" >/dev/null; then
printf 'Account already exists: %s\n' "$account" >&2
exit 1
fi
done
for team in proj_shop shop_read shop_ops; do
if getent group "$team" >/dev/null; then
printf 'Group already exists: %s\n' "$team" >&2
exit 1
fi
done
groupadd proj_shop
groupadd shop_read
groupadd shop_ops
useradd -m -U -s /bin/bash alice
useradd -m -U -s /bin/bash bob
useradd -m -U -s /bin/bash carol
useradd -r -M -U -d /nonexistent -s /usr/sbin/nologin deploy_shop
useradd -r -M -U -d /nonexistent -s /usr/sbin/nologin svc_shop
usermod -aG proj_shop,shop_read alice
usermod -aG shop_read bob
usermod -aG shop_ops,shop_read carol
2. Verify Personal Accounts and Membership
The script creates the personal users as well as the project groups. Verify the resulting database records, then enroll and test their intended authentication before changing remote-access policy.
getent passwd alice bob carol deploy_shop svc_shop
The -aG form appends supplementary memberships; -G without -a removes memberships omitted from the list. The script has already applied the required groups. Check the records, then verify the groups in a fresh login session.
id alice
id bob
id carol
For SSH best practices (keys instead of passwords, no direct root login), pair this with the approach from our article on SSH key management and access sharing for small teams.
3. Separate Local Runtime Tasks from CI Login
The example’s deploy_shop and svc_shop are local task/runtime identities with nologin shells. They do not accept ordinary SSH command deployment. OpenSSH normally invokes the account login shell even for a forced command; nologin exits unsuccessfully.
getent passwd deploy_shop svc_shop
If remote CI needs SSH, design a separate automation identity with an appropriate executable shell and a reviewed fixed command, restricted key options, source restrictions where applicable and credential rotation. A forced command alone does not disable forwarding; review restrict/no-pty and the effective sshd policy. Validate inputs such as SSH_ORIGINAL_COMMAND rather than passing them to an arbitrary shell. No CI SSH account is configured by this recipe.
4. Set Code, Data and Log Permissions
Create a fresh shop tree. Existing production trees require a separate ownership/ACL/symlink inventory and migration plan; do not recursively overwrite their modes.
The complete setup separates /srv/shop/prod/app, shared and logs. Parent directories permit traversal; leaf directories enforce access to project content. The deployment account owns code, while writable application data stays separate. The synthetic index.html is only an exercise fixture.
#!/bin/sh
set -eu
if [ -e /srv/shop ] || [ -L /srv/shop ]; then
printf '%s\n' '/srv/shop already exists; review it instead of overwriting permissions.' >&2
exit 1
fi
install -d -o root -g root -m 0755 /srv/shop /srv/shop/prod
install -d -o deploy_shop -g proj_shop -m 2770 /srv/shop/prod/app
install -d -o svc_shop -g proj_shop -m 2770 /srv/shop/prod/shared
install -d -o svc_shop -g shop_read -m 2750 /srv/shop/prod/logs
setfacl -m u:svc_shop:rx,g:shop_read:rx,m::rwx /srv/shop/prod/app
setfacl -d -m u::rwx,u:svc_shop:rx,g::rwx,g:shop_read:rx,m::rwx,o::--- /srv/shop/prod/app
setfacl -m u:svc_shop:rwx,m::rwx /srv/shop/prod/shared
setfacl -d -m u::rwx,u:svc_shop:rwx,g::rwx,m::rwx,o::--- /srv/shop/prod/shared
setfacl -d -m u::rwx,g::rx,m::rx,o::--- /srv/shop/prod/logs
install -o deploy_shop -g proj_shop -m 0660 /dev/null /srv/shop/prod/app/index.html
printf '%s\n' '<!doctype html><title>Shop preview</title><p>Synthetic tutorial page.</p>' > /srv/shop/prod/app/index.html
setfacl -m u:svc_shop:r,g:shop_read:r,m::rw /srv/shop/prod/app/index.html
The intended matrix is now concrete; verify it rather than infer it from a directory name or one numeric mode:
Verify the synthetic example from an administrative session. The first two reads should succeed; the last two writes should be denied. Unexpected success can indicate an additional broad group, sudo or ACL grant.
getfacl -p /srv/shop/prod/app /srv/shop/prod/app/index.html
sudo -u bob -- cat /srv/shop/prod/app/index.html
sudo -u svc_shop -- cat /srv/shop/prod/app/index.html
sudo -u bob -- touch /srv/shop/prod/app/bob-must-not-write
sudo -u svc_shop -- touch /srv/shop/prod/app/runtime-must-not-write
Adding/removing a group does not retroactively replace supplementary groups in existing processes. Validate grants in a new session; during revocation also inspect live sessions, processes, user-owned files and other credentials. id bob queries account data; id inside an old session may show a different group set. Membership in a reader group does not revoke write permission obtained from a writer group.
5. Verify File-Creation Policy
Keep creation policy local to the relevant shell, service or application. A global interactive profile edit does not configure every daemon, and may broaden permissions for unrelated users.
Without a default ACL, a file requested with mode 0666 becomes 0664 under umask 0002 and 0660 under umask 0007. Umask removes permissions; it does not add them or change existing files. The command below affects only the current shell and its children. Do not change the server-wide /etc/profile.d policy merely for this exercise. Review each service’s UMask= and default-ACL behavior separately.
umask 0007
The explicit mode requested by an application, inherited ACL and later chmod/copy/rename operations still matter. Recheck effective permissions after deployment and when the application changes its file-creation behavior.
6. Install Exact sudo Rules
Use a separate included file, but verify that the main sudoers file includes its directory. Edit with visudo, then validate the complete configuration and check actual allowed/denied commands. First install the fixed nonroot task:
Do not execute developer-controlled builds, deployment code or migrations as root. For a limited RunAs example, save the following as shop-check. It only computes a fixed file’s SHA-256; it does not deploy code or establish that the file is trusted. A real deployment still needs artifact verification, an atomic switch and rollback design.
#!/bin/sh
set -eu
if [ "$#" -ne 0 ]; then
printf '%s\n' 'No arguments are accepted.' >&2
exit 2
fi
exec /usr/bin/sha256sum -- /srv/shop/prod/app/index.html
After reviewing its contents, install it as an administrator/root. Protect the file and every parent directory against replacement by the users receiving permission:
install -d -o root -g root -m 0755 /usr/local/libexec
install -o root -g root -m 0755 ./shop-check /usr/local/libexec/shop-check
visudo -f /etc/sudoers.d/shop
Project-scoped operations
The included file is /etc/sudoers.d/shop; it must have trusted ownership and restrictive permissions. Validate the main configuration after editing it.
Use exact executable paths and arguments. The following groups are created by the account recipe:
This file is scoped to one project. shop_ops is an explicitly project-scoped group; a generic operations group does not automatically intersect project membership. Verify actual executable paths and avoid dots/backup suffixes in the include filename:
User_Alias SHOP_DEVS = %proj_shop
User_Alias SHOP_OPS = %shop_ops
Cmnd_Alias SHOP_VERIFY = /usr/local/libexec/shop-check ""
Cmnd_Alias SHOP_RESTART = /usr/bin/systemctl restart shop.service
Cmnd_Alias SHOP_LOGS = /usr/bin/journalctl --no-pager --unit shop.service --lines 100
SHOP_DEVS ALL=(deploy_shop) NOPASSWD: SHOP_VERIFY
SHOP_OPS ALL=(root) SHOP_RESTART, SHOP_LOGS
- This rule grants
proj_shoponly the fixedshop-checktask asdeploy_shop. The trailing""prohibits arguments; omitting arguments from a sudoers command generally allows arbitrary arguments. shop_opsmay run only the exact restart/log commands as root. No command-argument wildcards are used; additional units or options fall outside the rule.--no-pageravoids an interactive pager, but log contents may still be sensitive. The line limit and unit selection do not replace data-access policy.- Only the limited nonroot task has
NOPASSWD. Service operations use the default authentication policy; cached credentials and other rules mean a fresh password prompt is not guaranteed every time. - Other sudoers files, polkit rules, groups/ACLs and executable behavior contribute to effective privilege. This fragment does not prove that users have no other root path.
The sudoers manual’s argument, RunAs and include rules define these boundaries.
Check the complete configuration; parsing one file alone may miss conflicting aliases or wider grants elsewhere. The following -l checks do not execute the commands. The first two authorization queries should succeed and the final one should be denied:
chown root:root /etc/sudoers.d/shop
chmod 0440 /etc/sudoers.d/shop
visudo -c
sudo -l -U alice -u deploy_shop /usr/local/libexec/shop-check
sudo -l -U carol /usr/bin/systemctl restart shop.service
sudo -l -U bob /usr/bin/systemctl restart shop.service
Then, in an alice session, sudo -u deploy_shop /usr/local/libexec/shop-check should work; an extra argument or -u root must be denied. Test the task’s behavior and file permissions as well: visudo does not prove them.
Restart permission affects every project served by that unit. Restarting shared nginx, PHP-FPM or Supervisor is not project isolation. A project boundary needs a separate unit and runtime identity, with shared dependencies reviewed separately. Protect the unit, drop-ins, ExecStartPre/ExecStart/ExecReload chain, scripts, critical configuration and their parent directories against writes by delegated users. systemd command prefixes such as +/! can change credential boundaries.
The optional shop.service below exposes the synthetic page only on localhost in an isolated exercise. Python http.server is not recommended for production. For this optional section, first confirm that the target has an executable /usr/bin/python3 and the http.server module; minimal images may omit them. If needed, install the distribution’s supported Python 3 packages on the test server. Configure an appropriate application server, dependencies, access controls, health checks and rollback for a real workload; this permission exercise is not a production architecture.
[Unit]
Description=Shop static preview for an isolated tutorial
After=network.target
[Service]
Type=simple
User=svc_shop
Group=svc_shop
WorkingDirectory=/srv/shop/prod/app
ExecStart=/usr/bin/python3 -m http.server 18080 --bind 127.0.0.1 --directory /srv/shop/prod/app
UMask=0027
NoNewPrivileges=yes
PrivateTmp=yes
ProtectHome=yes
ProtectSystem=strict
ReadOnlyPaths=/srv/shop/prod/app
ReadWritePaths=/srv/shop/prod/shared /srv/shop/prod/logs
[Install]
WantedBy=multi-user.target
Install the file as root:root, mode 0644, at /etc/systemd/system/shop.service. Inspect and resolve every diagnostic from systemd-analyze verify /etc/systemd/system/shop.service; its default exit status alone is insufficient. Then start it only on the test server with systemctl daemon-reload and systemctl start shop.service, then check actual process credentials, read/write behavior and the localhost response. Syntax acceptance is not a running-service test. With Type=simple, a successful start result alone also does not prove that Python ran or answered HTTP. ProtectSystem=strict and writable exclusions do not replace Unix permissions; PrivateTmp also provides private temporary writable storage. Do not read ReadWritePaths as an absolute boundary for every filesystem interaction.
The systemd.exec documentation details User, UMask and filesystem restrictions. Checking syntax and equivalent process permissions does not validate the running service; test service startup, SSH connections and production-load acceptance separately.
Database Tasks and Privilege Boundaries
Keep database authorization separate from the project service rule. An interactive SQL client is not a constrained root maintenance operation.
For a real backup task, define both OS and database permissions and protect its output. For example:
Design database authorization separately. Do not allow interactive mysql/psql clients or generic dump tools as root with arbitrary arguments: shell, file and connection capabilities can expand the OS privilege boundary. A backup needs a restricted OS identity, appropriate database role, output-file controls and retention policy. If a named connection service has already been configured for your actual system, the account may invoke it as follows:
umask 0077
pg_dump --dbname=service=shop_backup --format=custom --file="$HOME/shop.dump"
This command does not presume root access; the shop_backup connection service is not created by this tutorial. TLS, authentication, database/schema/table privileges, output capacity and restore testing are separate prerequisites. A role name grants no privilege by itself, and a single-database dump does not back up cluster-wide roles/tablespaces. Validate dump/restore separately in your own test environment.
A password prompt does not turn an unrestricted database client into a safe root sandbox. Reduce the target identity and task scope first, then define the required authentication policy.
Ops / full admin sudo rules
visudo -f /etc/sudoers.d/global-ops
Only for a separately created and carefully controlled global_ops group, the following grants full host administration. It is not part of the shop exercise and is not a restricted web/DB role:
%global_ops ALL=(ALL:ALL) ALL
Limit and review global_ops membership and retain tested recovery access. Its members can become root; the project rules cannot constrain them. Account locking or group removal alone may leave existing processes, sessions or other credentials active.
7. Tie it into your security hardening
User, group and sudo design does not live in a vacuum. It should be part of a broader server security baseline: SSH hardening, firewall rules, automatic updates and log monitoring. Our VPS security hardening checklist with sshd_config, Fail2ban and disabling root SSH goes through these steps in detail and complements the architecture we have just built.
Security Best Practices and Common Pitfalls
Review these permission and lifecycle mistakes as the model evolves. Test negative cases as deliberately as successful access.
Sharing a deploy or admin login between people obscures attribution and complicates offboarding. Keep personal accounts for humans, and use dedicated, narrowly scoped identities for each automation boundary. Review keys, tokens, active sessions and owned files when access ends.
Never “fix” permissions with 777
When a web app fails with a “permission denied” error, a common quick‑and‑dirty fix is to run:
chmod -R 777 /srv/corp-site/prod
This broad recursive mode permits other local users to modify the tree and adds execute bits to regular files. Repair the actual owner/group, ACL, parent traversal and file-type requirements instead; inspect ACLs and privileged users separately. See safe Linux file permissions on VPS for context.
Disable direct root SSH login
Before disabling direct root SSH, confirm a personal administrator’s key-based login and required sudo in a second session. Keep the current administrative session, a working console/recovery path and a configuration backup.
PubkeyAuthentication yes
AuthenticationMethods publickey
PasswordAuthentication no
KbdInteractiveAuthentication no
PermitRootLogin no
This is a publickey-only policy, not MFA; do not replace an existing key-plus-second-factor requirement with it. Password and keyboard-interactive authentication are distinct methods. Do not blindly append directives: Include ordering and relevant Match blocks affect effective values. Review the target version with sshd -t and sshd -T plus appropriate -C connection parameters, preserve PAM account/session controls, then apply the distribution’s service/socket procedure. Keep recovery access while testing new successful and rejected connections and sudo. A syntax check is not an SSH handshake test.
Separate staging from production
Use separate groups and also separate deploy/runtime identities for prod and stage. A shared owner, service account, credential or writable startup configuration can cross a group-only boundary. Test both directions and protect secrets; separate VPS instances may be appropriate where the required isolation exceeds a shared host.
Combine with a firewall and log monitoring
Combine the account design with SSH policy, a reviewed firewall and log monitoring. These controls address different paths; none makes the others irrelevant. Our VPS firewall guide provides related network controls.
Scaling the Model: From One VPS to Many
Reuse the naming and policy structure as you add hosts, while validating each host’s effective accounts, IDs, paths, application and operational requirements.
Standardise your pattern
Once you are happy with the layout on one VPS, document your conventions:
- Project/environment account and group naming.
- Separate deployment and runtime ownership plus mutable data paths.
- Actual sudo command/RunAs rules and their trusted dependencies.
- Permission/ACL verification, rollback, offboarding and review owners.
Then reuse this pattern on every new VPS or dedicated server you provision at dchost.com. Consistency makes troubleshooting and onboarding much easier.
Automate with Ansible and Terraform
Manually running useradd, groupadd and chown is fine for the first server. When you have several, you will want automation. A common pattern:
- Use Terraform or a similar tool with a supported provider or provisioning integration to create the required hosts.
- Use Ansible to configure users, groups, sudoers, directories and permissions from structured project, environment and team data.
Our guide to automating VPS setup with Terraform and Ansible explains this division of work. Keep syntax checks, access tests, rollback and drift review alongside the configuration.
Consider central authentication for larger teams
For larger fleets, a managed directory, LDAP/FreeIPA and clients such as SSSD may centralize identity and policy. Integration, local fallback, cache expiry, nested-group behavior and revocation depend on the selected system; local Unix group files do not gain nesting automatically. Test outages and removed-user access rather than assume changes are pushed instantly to every process.
A Maintainable VPS Access Model
A useful access model identifies who can read code, publish a release, change runtime data or operate a service, and under which identity. Keep one reviewed policy and acceptance matrix per project/environment. Rerun relevant permission and authorization checks after account, deployment-tool, unit or application changes; also review the logs needed to attribute operations.
For a new project or hosting migration, record these requirements before granting access. Pair this model with the VPS hardening checklist and, where needed, Terraform and Ansible automation. Confirm the management responsibilities included in the service you buy. Personal accounts and scoped commands help make access reviewable; they do not promise complete isolation or a full audit trail by themselves.





