Movatterモバイル変換


[0]ホーム

URL:


Skip to main content

SSH and sudo

Host-level access controls are an important part of every organization'ssecurity strategy. UsingLinux-PAM and OPAwe can extend policy-based access control to SSH and sudo.

Goals

This tutorial shows how you can use OPA and Linux-PAM to enforce fine-grained,host-level access controls over SSH and sudo.

Linux-PAM can be configured to delegate authorization decisions to plugins(shared libraries). In this case, we have created an OPA-based plugin that canbe configured to authorize SSH and sudo access. The OPA-based Linux-PAM pluginused in this tutorial can be found atopen-policy-agent/contrib.

For this tutorial, our desired policy is:

  • Admins can SSH into any host and run sudo commands.
  • Normal users can SSH into hosts that they havecontributed to and run sudo commands.

Furthermore, we'll assume we have the following set of users and hosts:

  • frontend-dev is a developer who contributes to the app running on thefrontend host.
  • backend-dev is a developer who contributes to the app running on thebackend host.
  • ops is an administrator for the organization.

Authentication (verifying user identity) is outside the scope of OPA'sresponsibility so this tutorial relies on identities being staticallydefined. In real-world scenarios authentication can be delegated to SSH itself(authorized_keys) or other identity management systems.

Let's get started.

Prerequisites

This tutorial requiresDocker Compose to run dummy SSH hosts alongwith OPA. The dummy SSH hosts are just containers running sshd inside.

Steps

1. Bootstrap the tutorial environment using Docker Compose.

First, create atutorial-docker-compose.yaml file that runs OPA and the containers thatrepresent our backend and frontend hosts.

tutorial-docker-compose.yaml
version:"2"
services:
opa:
image: openpolicyagent/opa:1.6.0
ports:
-"8181:8181"
# WARNING: OPA is NOT running with an authorization policy configured. This
# means that clients can read and write policies in OPA. If you are
# deploying OPA in an insecure environment, be sure to configure
# authentication and authorization on the daemon. See the Security page for
# details: https://www.openpolicyagent.org/docs/security.html.
command:
-"run"
-"--server"
-"--set=decision_logs.console=true"
-"--set=services.nginx.url=http://bundle_server"
-"--set=bundles.nginx.service=nginx"
-"--set=bundles.nginx.resource=bundles/bundle.tar.gz"
depends_on:
- bundle_server
frontend:
image: openpolicyagent/demo-pam
ports:
-"2222:22"
volumes:
- ./frontend_host_id.json:/etc/host_identity.json
backend:
image: openpolicyagent/demo-pam
ports:
-"2223:22"
volumes:
- ./backend_host_id.json:/etc/host_identity.json
bundle_server:
image: nginx:1.20.0-alpine
ports:
- 8888:80
volumes:
- ./bundles:/usr/share/nginx/html/bundles

Thetutorial-docker-compose.yaml file requires two other local files:frontend_host_id.json andbackend_host_id.json. These files are mountedinto the containers representing our hosts. The content of the file providescontext that the PAM module provides as input when executing queriesagainst OPA.

Create the extra files required by tutorial-docker-compose.yaml:

echo '{"host_id": "frontend"}' > frontend_host_id.json
echo '{"host_id": "backend"}' > backend_host_id.json

In real-world scenarios, these files could contain arbitrary information that we want to expose to the policy.

Finally, rundocker-compose to pull and run the containers.

docker-compose -f tutorial-docker-compose.yaml up

This tutorial uses a special Docker image namedopenpolicyagent/demo-pam to simulate an SSH server.This image contains pre-created Linux accounts for our users, and the required PAM module ispre-configured inside thesudo andsshd files in/etc/pam.d/.

2. Create a Bundle for the policies and data.

In another terminal, create the policies and data that OPA will use to control access to the hosts.

First, create folder called bundles and cd into it.

mkdir bundles
cd bundles

Next, create a policy that will tell the PAM module to collect context that is required for authorization.For more details on what this policy should look like, seethis documentation.

pull.rego:

package pull

# Which files should be loaded into the context?
files:=["/etc/host_identity.json"]

# Which environment variables should be loaded into the context?
env_vars:=[]

Create the policies that will authorize SSH and sudo requests.Theinput which makes up the authorization context in the policy below will alsoinclude some default values, such as the username making the request. Seethis documentationto get a better understanding of what theinput to the authorization policy will look like.

Unlike thepull policy, we'll create separateauthz policiesfor SSH andsudo for more fine-grained control.In production, it makes more sense to have this separation fordisplay andpull as well.

Create the SSH authorization policy. It should allow admins to SSH into all hosts,and non-admins to only SSH into hosts that they contributed code to.

sshd_authz.rego:

package sshd.authz

import input.pull_responses
import input.sysinfo

import data.hosts

# By default, users are not authorized.
default allow:=false

# Allow access to any user that has the "admin" role.
allow if{
data.roles.admin[_]== input.sysinfo.pam_username
}

# Allow access to any user who contributed to the code running on the host.
#
# This rule gets the "host_id" value from the file "/etc/host_identity.json".
# It is available in the input under "pull_responses" because we
# asked for it in our pull policy above.
#
# It then compares all the contributors for that host against the username
# that is asking for authorization.
allow if{
hosts[pull_responses.files["/etc/host_identity.json"].host_id].contributors[_]== sysinfo.pam_username
}

# If the user is not authorized, then include an error message in the response.
errors contains"Request denied by administrative policy" if{
not allow
}

Create thesudo authorization policy. It should allow only admins to usesudo.

sudo_authz.rego:

package sudo.authz

# By default, users are not authorized.
default allow:=false

# Allow access to any user that has the "admin" role.
allow if{
data.roles.admin[_]== input.sysinfo.pam_username
}

# If the user is not authorized, then include an error message in the response.
errors contains"Request denied by administrative policy" if{
not allow
}

Now we need to create the data that represents our roles, hots, and contributors into OPA.

Create a folder called roles, and the following data file.

mkdir roles
cat <<EOF > roles/data.json
{
"admin": ["ops"]
}
EOF

Create a folder called hosts, and the following data file.

mkdir hosts
cat <<EOF > hosts/data.json
{
"frontend": {
"contributors": [
"frontend-dev"
]
},
"backend": {
"contributors": [
"backend-dev"
]
}
}
EOF

Finally create the bundle for the bundle server to use.

opa build -b .

Now you should have the following file structure setup.

.
└── tutorial-docker-compose.yaml
├── backend_host_id.json
├── frontend_host_id.json
├── bundles
│   ├── bundle.tar.gz
│   ├── pull.rego
│   ├── sshd_authz.rego
│   ├── sudo_authz.rego
│   ├── hosts
│   │   └── data.json
│   ├── roles
│   │   └── data.json

3. SSH and sudo as a user with theadmin role.

First, let's try to access the hosts as theops user. Recall, theops userhas been granted theadmin role (via thePUT /data/roles request above) andusers with theadmin role can login to any host and perform sudo commands.

Login to thefrontend host (which has SSH listening on port 2222) and run a command with sudo as theops user.

ssh -p 2222 ops@localhost \
-o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null

sudo ls /
exit

You will see a lot of verbose logs fromsudo as the PAM module goes through the motions.This is intended so you can study how the PAM module works.You can disable verbose logging by changing thelog_level argument in the PAMconfiguration. For more details seethis documentation.

4. SSH as a user without theadmin role.

Let's try a user without the admin role. Recall, that a non-admin user can SSHinto any host that they havecontributed to.

Thefrontend-dev user contributed code to thefrontend host so they should beable to login.

ssh -p 2222 frontend-dev@localhost \
-o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null

Only admins can usesudo, so you shouldn't be able to runsudo ls /.

Sincefrontend-dev did not contribute to the code running on thebackend host (which has SSH listening on port 2223), they should not be ableto login.

ssh -p 2223 frontend-dev@localhost \
-o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null

5. Elevate a user's rights through policy.

Suppose you have a ticketing system for elevation, where you generate tickets for usersthat need elevated rights, send the ticket to the user, and expire those tickets whentheir rights should be removed.

Let's mock the current state of this simple ticketing system's API with some data.

mkdir elevate
cat <<EOF > elevate/data.json
{
"tickets": {
"frontend-dev": "1234"
}
}
EOF

This means that for now, if thefrontend-dev user can provide ticket number1234,they should be able to SSH into all servers.

Let's write policy to ensure that this happens.

First, we need to make the PAM module take input from the user.

display.rego:

package display

# What should be prompted to the user?
display_spec:=[
{
"message":"Please enter an elevation ticket if you have one:",
"style":"prompt_echo_on",
"key":"ticket"
}
]

Then we need to make sure that the authorization takes this input into account.

sudo_authz_elevated.rego:

# A package can be defined across multiple files.
package sudo.authz

import data.elevate
import input.display_responses
import input.sysinfo

# Allow this user if the elevation ticket they provided matches our mock API
# of an internal elevation system.
allow if{
elevate.tickets[sysinfo.pam_username]== display_responses.ticket
}

Now we need to build a new bundle for OPA to use.

opa build -b .

Confirm that the userfrontend-dev can indeed usesudo.

ssh -p 2222 frontend-dev@localhost \
-o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null

sudo ls /

You should be prompted with the message that we defined in ourdisplay policyfor both the SSH andsudo authorization cycles.This happens because thedisplay policy is shared by the PAM configurations of SSH andsudo.In production, it is more practical to use separate policy packages for each PAM configuration.

We have not defined the SSHauthz policy to work with elevation, so you can enter any valueinto the prompt that comes up for for SSH.

Forsudo, enter the ticket number1234 to get access.

Lastly, update the mocked elevation API and confirm the user's original rights are restored.

cat <<EOF > elevate/data.json
{
"tickets": {}
}
EOF

Once again, build the bundle with this new data

opa build -b .

You will find that runningsudo ls / as thefrontend-dev user is disallowed again.

It is possible to configure thedisplay policy to only make the PAM module prompt for theelevation ticket when our mock API has a non-emptytickets object. So when there are noelevated users, there will be no prompt for a ticket. This can be done using the Regocount aggregate.

Wrap Up

Congratulations for finishing the tutorial!

You learned a number of things about SSH with OPA:

  • OPA gives you fine-grained access control over SSH,sudo, and any other application that uses PAM.Although this tutorial used the some of the same policies for bothSSH and sudo, you should use separate, fine-grained policies for each application that supports PAM.
  • Writing allow/deny policies to control who has access to what using context from the user and host.
  • Importing external data into OPA and writing policies that depend on that data.

The code for the PAM module used in this tutorial can be found in theopen-policy-agent/contribrepository.


[8]ページ先頭

©2009-2025 Movatter.jp