1"use strict";(self.webpackChunkopa_website=self.webpackChunkopa_website||[]).push([[16999],{28453:(e,n,s)=>{s.d(n,{R:()=>r,x:()=>a});var o=s(96540);const t={},i=o.createContext(t);function r(e){const n=o.useContext(i);return o.useMemo((function(){return"function"==typeof e?e(n):{...n,...e}}),[n,e])}function a(e){let n;return n=e.disableParentContext?"function"==typeof e.components?e.components(t):e.components||t:r(e.components),o.createElement(i.Provider,{value:n},e.children)}},73141:(e,n,s)=>{s.r(n),s.d(n,{assets:()=>l,contentTitle:()=>a,default:()=>h,frontMatter:()=>r,metadata:()=>o,toc:()=>d});const o=JSON.parse('{"id":"ssh-and-sudo-authorization","title":"SSH and sudo","description":"Host-level access controls are an important part of every organization\'s","source":"@site/docs/ssh-and-sudo-authorization.md","sourceDirName":".","slug":"/ssh-and-sudo-authorization","permalink":"/docs/ssh-and-sudo-authorization","draft":false,"unlisted":false,"tags":[],"version":"current","frontMatter":{"title":"SSH and sudo"},"sidebar":"docsSidebar","previous":{"title":"Kafka","permalink":"/docs/kafka-authorization"},"next":{"title":"Terraform","permalink":"/docs/terraform"}}');var t=s(74848),i=s(28453);const r={title:"SSH and sudo"},a=void 0,l={},d=[{value:"Goals",id:"goals",level:2},{value:"Prerequisites",id:"prerequisites",level:2},{value:"Steps",id:"steps",level:2},{value:"1. Bootstrap the tutorial environment using Docker Compose",id:"1-bootstrap-the-tutorial-environment-using-docker-compose",level:3},{value:"2. Create a Bundle for the policies and data",id:"2-create-a-bundle-for-the-policies-and-data",level:3},{value:"3. SSH and sudo as a user with the <code>admin</code> role",id:"3-ssh-and-sudo-as-a-user-with-the-admin-role",level:3},{value:"4. SSH as a user without the <code>admin</code> role",id:"4-ssh-as-a-user-without-the-admin-role",level:3},{value:"5. Elevate a user's rights through policy",id:"5-elevate-a-users-rights-through-policy",level:3},{value:"Summary",id:"summary",level:2}];function c(e){const n={a:"a",blockquote:"blockquote",code:"code",em:"em",h2:"h2",h3:"h3",li:"li",p:"p",pre:"pre",strong:"strong",ul:"ul",...(0,i.R)(),...e.components},{EvergreenCodeBlock:s}=n;return s||function(e,n){throw new Error("Expected "+(n?"component":"object")+" `"+e+"` to be defined: you likely forgot to import, pass, or provide it.")}("EvergreenCodeBlock",!0),(0,t.jsxs)(t.Fragment,{children:[(0,t.jsxs)(n.p,{children:["Host-level access controls are an important part of every organization's\nsecurity strategy. With ",(0,t.jsx)(n.a,{href:"https://tldp.org/HOWTO/User-Authentication-HOWTO/x115.html",children:"Linux-PAM"})," and OPA,\npolicy-based access control can be extended to SSH and sudo."]}),"\n",(0,t.jsx)(n.h2,{id:"goals",children:"Goals"}),"\n",(0,t.jsx)(n.p,{children:"This tutorial shows how you can use OPA and Linux-PAM to enforce fine-grained,\nhost-level access controls over SSH and sudo."}),"\n",(0,t.jsxs)(n.p,{children:["Linux-PAM can be configured to delegate authorization decisions to plugins\n(shared libraries). An OPA-based plugin has been created that can\nbe configured to authorize SSH and sudo access. The OPA-based Linux-PAM plugin\nused in this tutorial can be found at ",(0,t.jsx)(n.a,{href:"https://github.com/open-policy-agent/contrib/tree/main/pam_opa",children:"open-policy-agent/contrib"}),"."]}),"\n",(0,t.jsx)(n.p,{children:"For this tutorial, the desired policy is:"}),"\n",(0,t.jsxs)(n.ul,{children:["\n",(0,t.jsx)(n.li,{children:"Admins can SSH into any host and run sudo commands."}),"\n",(0,t.jsxs)(n.li,{children:["Normal users can SSH into hosts that they have ",(0,t.jsx)(n.em,{children:"contributed"})," to and run sudo commands."]}),"\n"]}),"\n",(0,t.jsx)(n.p,{children:"The following set of users and hosts is assumed:"}),"\n",(0,t.jsxs)(n.ul,{children:["\n",(0,t.jsxs)(n.li,{children:[(0,t.jsx)(n.code,{children:"frontend-dev"})," is a developer who contributes to the app running on the ",(0,t.jsx)(n.code,{children:"frontend"})," host."]}),"\n",(0,t.jsxs)(n.li,{children:[(0,t.jsx)(n.code,{children:"backend-dev"})," is a developer who contributes to the app running on the ",(0,t.jsx)(n.code,{children:"backend"})," host."]}),"\n",(0,t.jsxs)(n.li,{children:[(0,t.jsx)(n.code,{children:"ops"})," is an administrator for the organization."]}),"\n"]}),"\n",(0,t.jsxs)(n.p,{children:["Authentication (verifying user identity) is outside the scope of OPA's\nresponsibility so this tutorial relies on identities being statically\ndefined. In real-world scenarios authentication can be delegated to SSH itself\n(",(0,t.jsx)(n.code,{children:"authorized_keys"}),") or other identity management systems."]}),"\n",(0,t.jsx)(n.h2,{id:"prerequisites",children:"Prerequisites"}),"\n",(0,t.jsxs)(n.p,{children:["This tutorial requires ",(0,t.jsx)(n.a,{href:"https://docs.docker.com/compose/install/",children:"Docker Compose"})," to run dummy SSH hosts along\nwith OPA. The dummy SSH hosts are just containers running sshd inside."]}),"\n",(0,t.jsx)(n.h2,{id:"steps",children:"Steps"}),"\n",(0,t.jsx)(n.h3,{id:"1-bootstrap-the-tutorial-environment-using-docker-compose",children:"1. Bootstrap the tutorial environment using Docker Compose"}),"\n",(0,t.jsxs)(n.p,{children:["First, create a ",(0,t.jsx)(n.code,{children:"tutorial-docker-compose.yaml"})," file that runs OPA and the containers that\nrepresent the backend and frontend hosts."]}),"\n",(0,t.jsx)(s,{children:(0,t.jsx)(n.pre,{children:(0,t.jsx)(n.code,{className:"language-yaml",metastring:'title="tutorial-docker-compose.yaml"',children:'version: "2"\nservices:\n opa:\n image: openpolicyagent/opa:{{ current_version_docker }}\n ports:\n - "8181:8181"\n # WARNING: OPA is NOT running with an authorization policy configured. This\n # means that clients can read and write policies in OPA. If you are\n # deploying OPA in an insecure environment, be sure to configure\n # authentication and authorization on the daemon. See the Security page for\n # details: https://www.openpolicyagent.org/docs/security.html.\n command:\n - "run"\n - "--server"\n - "--addr=0.0.0.0:8181"\n - "--set=
1decision_logs.console=true"\n - "--set=services.nginx.url=http://bundle_server"\n - "--set=bundles.nginx.service=nginx"\n - "--set=bundles.nginx.resource=bundles/bundle.tar.gz"\n depends_on:\n - bundle_server\n frontend:\n image: openpolicyagent/demo-pam\n ports:\n - "2222:22"\n volumes:\n - ./frontend_host_id.json:/etc/host_identity.json\n backend:\n image: openpolicyagent/demo-pam\n ports:\n - "2223:22"\n volumes:\n - ./backend_host_id.json:/etc/host_identity.json\n bundle_server:\n image: nginx:1.20.0-alpine\n ports:\n - 8888:80\n volumes:\n - ./bundles:/usr/share/nginx/html/bundles\n'})})}),"\n",(0,t.jsxs)(n.p,{children:["The ",(0,t.jsx)(n.code,{children:"tutorial-docker-compose.yaml"})," file requires two other local files:\n",(0,t.jsx)(n.code,{children:"frontend_host_id.json"})," and ",(0,t.jsx)(n.code,{children:"backend_host_id.json"}),". These files are mounted\ninto the containers representing the hosts. The content of the file provides\n",(0,t.jsx)(n.em,{children:"context"})," that the PAM module provides as input when executing queries\nagainst OPA."]}),"\n",(0,t.jsx)(n.p,{children:"Create the extra files required by tutorial-docker-compose.yaml:"}),"\n",(0,t.jsx)(n.pre,{children:(0,t.jsx)(n.code,{className:"language-shell",children:'echo \'{"host_id": "frontend"}\' > frontend_host_id.json\necho \'{"host_id": "backend"}\' > backend_host_id.json\n'})}),"\n",(0,t.jsxs)(n.blockquote,{children:["\n",(0,t.jsx)(n.p,{children:"In real-world scenarios, these files could contain arbitrary information to expose to the policy."}),"\n"]}),"\n",(0,t.jsxs)(n.p,{children:["Finally, run ",(0,t.jsx)(n.code,{children:"docker-compose"})," to pull and run the containers."]}),"\n",(0,t.jsx)(n.pre,{children:(0,t.jsx)(n.code,{className:"language-shell",children:"docker-compose -f tutorial-docker-compose.yaml up\n"})}),"\n",(0,t.jsxs)(n.p,{children:["This tutorial uses a special Docker image named ",(0,t.jsx)(n.code,{children:"openpolicyagent/demo-pam"})," to simulate an SSH server.\nThis image contains pre-created Linux accounts for the users, and the required PAM module is\npre-configured inside the ",(0,t.jsx)(n.code,{children:"sudo"})," and ",(0,t.jsx)(n.code,{children:"sshd"})," files in ",(0,t.jsx)(n.code,{children:"/etc/pam.d/"}),"."]}),"\n",(0,t.jsx)(n.h3,{id:"2-create-a-bundle-for-the-policies-and-data",children:"2. Create a Bundle for the policies and data"}),"\n",(0,t.jsx)(n.p,{children:"In another terminal, create the policies and data that OPA will use to control access to the hosts."}),"\n",(0,t.jsx)(n.p,{children:"First, create folder called bundles and cd into it."}),"\n",(0,t.jsx)(n.pre,{children:(0,t.jsx)(n.code,{className:"language-bash",children:"mkdir bundles\ncd bundles\n"})}),"\n",(0,t.jsxs)(n.p,{children:["Next, create a policy that will tell the PAM module to collect context that is required for authorization.\nFor more details on what this policy should look like, see ",(0,t.jsx)(n.a,{href:"https://github.com/open-policy-agent/contrib/tree/main/pam_opa/pam#pull",children:"this documentation"}),"."]}),"\n",(0,t.jsxs)(n.p,{children:[(0,t.jsx)(n.strong,{children:"pull.rego"}),":"]}),"\n",(0,t.jsx)(n.pre,{children:(0,t.jsx)(n.code,{className:"language-rego",children:'package pull\n\n# Which files should be loaded into the context?\nfiles := ["/etc/host_identity.json"]\n\n# Which environment variables should be loaded into the context?\nenv_vars := []\n'})}),"\n",(0,t.jsxs)(n.p,{children:["Create the policies that will authorize SSH and sudo requests.\nThe ",(0,t.jsx)(n.code,{children:"input"})," which makes up the authorization context in the policy below will also\ninclude some default values, such as the username making the request. See\n",(0,t.jsx)(n.a,{href:"https://github.com/open-policy-agent/contrib/tree/main/pam_opa/pam#authz",children:"this documentation"}),"\nto get a better understanding of what the ",(0,t.jsx)(n.code,{children:"input"})," to the authorization policy will look like."]}),"\n",(0,t.jsxs)(n.p,{children:["Unlike the ",(0,t.jsx)(n.em,{children:"pull"})," policy, the ",(0,t.jsx)(n.em,{children:"authz"})," policies for SSH and ",(0,t.jsx)(n.code,{children:"sudo"})," are kept separate for more fine-grained control.\nIn production, it makes more sense to have this separation for ",(0,t.jsx)(n.em,{children:"display"})," and ",(0,t.jsx)(n.em,{children:"pull"})," as well."]}),"\n",(0,t.jsx)(n.p,{children:"Create the SSH authorization policy. It should allow admins to SSH into all hosts,\nand non-admins to only SSH into hosts that they contributed code to."}),"\n",(0,t.jsxs)(n.p,{children:[(0,t.jsx)(n.strong,{children:"sshd_authz.rego"}),":"]}),"\n",(0,t.jsx)(n.pre,{children:(0,t.jsx)(n.code,{className:"language-rego",children:'package sshd.authz\n\nimport input.pull_responses\nimport input.sysinfo\n\nimport data.hosts\n\n# By default, users are not authorized.\ndefault allow := false\n\n# Allow access to any user that has the "admin" role.\nallow if {\n data.roles.admin[_] == input.sysinfo.pam_username\n}\n\n# Allow access to any user who contributed to the code running on the host.\n#\n# This rule gets the "host_id" value from the file "/etc/host_identity.json".\n# It is available in the input under "pull_responses" because we\n# asked for it in our pull policy above.\n#\n# It then compares all the contributors for that host against the username\n# that is asking for authorization.\nallow if {\n hosts[pull_responses.files["/etc/host_identity.json"].host_id].contributors[_] == sysinfo.pam_username\n}\n\n# If the user is not authorized, then include an error message in the response.\nerrors contains "Request denied by administrative policy" if {\n not allow\n}\n'})}),"\n",(0,t.jsxs)(n.p,{children:["Create the ",(0,t.jsx)(n.code,{children:"sudo"})," authorization policy. It should allow only admins to use ",(0,t.jsx)(n.code,{children:"sudo"}),"."]}),"\n",(0,t.jsxs)(n.p,{children:[(0,t.jsx)(n.strong,{children:"sudo_authz.rego"}),":"]}),"\n",(0,t.jsx)(n.pre,{children:(0,t.jsx)(n.code,{className:"language-rego",children:'package sudo.authz\n\n# By default, users are not authorized.\ndefault allow := false\n\n# Allow access to any user that has the "admin" role.\nallow if {\n data.roles.admin[_] == input.sysinfo.pam_username\n}\n\n# If the user is not authorized, then include an error message in the response.\nerrors contains "Request denied by administrative policy" if {\n not allow\n}\n'})}),"\n",(0,t.jsx)(n.p,{children:"Create the data that represents the roles, hosts, and contributors in OPA."}),"\n",(0,t.jsx)(n.p,{children:"Create a folder called roles, and the following data file."}),"\n",(0,t.jsx)(n.pre,{children:(0,t.jsx)(n.code,{className:"language-shell",children:'mkdir roles\ncat <<EOF > roles/data.json\n{\n "admin": ["ops"]\n}\nEOF\n'})}),"\n",(0,t.jsx)(n.p,{children:"Create a folder called hosts, and the following data file."}),"\n",(0,t.jsx)(n.pre,{children:(0,t.jsx)(n.code,{className:"language-shell",children:'mkdir hosts\ncat <<EOF > hosts/data.json\n{\n "frontend": {\n "contributors": [\n "frontend-dev"\n ]\n },\n "backend": {\n "contributors": [\n "backend-dev"\n ]\n }\n}\nEOF\n'})}),"\n",(0,t.jsx)(n.p,{children:"Finally create the bundle for the bundle server to use."}),"\n",(0,t.jsx)(n.pre,{children:(0,t.jsx)(n.code,{className:"language-bash",children:"opa build -b .\n"})}),"\n",(0,t.jsx)(n.p,{children:"Now you should have the following file structure setup."}),"\n",(0,t.jsx)(n.pre,{children:(0,t.jsx)(n.code,{children:".\n\u2514\u2500\u2500 tutorial-docker-compose.yaml\n\u251c\u2500\u2500 backend_host_id.json\n\u251c\u2500\u2500 frontend_host_id.json\n\u251c\u2500\u2500 bundles\n\u2502\xa0\xa0 \u251c\u2500\u2500 bundle.tar.gz\n\u2502\xa0\xa0 \u251c\u2500\u2500 pull.rego\n\u2502\xa0\xa0 \u251c\u2500\u2500 sshd_authz.rego\n\u2502\xa0\xa0 \u251c\u2500\u2500 sudo_authz.rego\n\u2502\xa0\xa0 \u251c\u2500\u2500 hosts\n\u2502\xa0\xa0 \u2502\xa0\xa0 \u2514\u2500\u2500 data.json\n\u2502\xa0\xa0 \u251c\u2500\u2500 roles\n\u2502\xa0\xa0 \u2502\xa0\xa0 \u2514\u2500\u2500 data.json\n"})}),"\n",(0,t.jsxs)(n.h3,{id:"3-ssh-and-sudo-as-a-user-with-the-admin-role",children:["3. SSH and sudo as a user with the ",(0,t.jsx)(n.code,{children:"admin"})," role"]}),"\n",(0,t.jsxs)(n.p,{children:["First, try to access the hosts as the ",(0,t.jsx)(n.code,{children:"ops"})," user. Recall, the ",(0,t.jsx)(n.code,{children:"ops"})," user\nhas been granted the ",(0,t.jsx)(n.code,{children:"admin"})," role (via the ",(0,t.jsx)(n.code,{children:"PUT /data/roles"})," request above) and\nusers with the ",(0,t.jsx)(n.code,{children:"admin"})," role can login to any host and perform sudo commands."]}),"\n",(0,t.jsxs)(n.p,{children:["Login to the ",(0,t.jsx)(n.code,{children:"frontend"})," host (which has SSH listening on port 2222) and run a command with sudo as the ",(0,t.jsx)(n.code,{children:"ops"})," user."]}),"\n",(0,t.jsx)(n.pre,{children:(0,t.jsx)(n.code,{className:"language-shell",children:"ssh -p 2222 ops@localhost \\\n -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null\n\nsudo ls /\nexit\n"})}),"\n",(0,t.jsxs)(n.p,{children:["You will see a lot of verbose logs from ",(0,t.jsx)(n.code,{children:"sudo"})," as the PAM module goes through the motions.\nThis is intended so you can study how the PAM module works.\nYou can disable verbose logging by changing the ",(0,t.jsx)(n.code,{children:"log_level"})," argument in the PAM\nconfiguration. For more details see\n",(0,t.jsx)(n.a,{href:"https://github.com/open-policy-agent/contrib/tree/main/pam_opa/pam#configuration",children:"this documentation"}),"."]}),"\n",(0,t.jsxs)(n.h3,{id:"4-ssh-as-a-user-without-the-admin-role",children:["4. SSH as a user without the ",(0,t.jsx)(n.code,{children:"admin"})," role"]}),"\n",(0,t.jsxs)(n.p,{children:["Try a user without the admin role. Recall, that a non-admin user can SSH\ninto any host that they have ",(0,t.jsx)(n.em,{children:"contributed to"}),"."]}),"\n",(0,t.jsxs)(n.p,{children:["The ",(0,t.jsx)(n.code,{children:"frontend-dev"})," user contributed code to the ",(0,t.jsx)(n.code,{children:"frontend"})," host so they should be\nable to login."]}),"\n",(0,t.jsx)(n.pre,{children:(0,t.jsx)(n.code,{className:"language-shell",children:"ssh -p 2222 frontend-dev@localhost \\\n -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null\n"})}),"\n",(0,t.jsxs)(n.p,{children:["Only admins can use ",(0,t.jsx)(n.code,{children:"sudo"}),", so you shouldn't be able to run ",(0,t.jsx)(n.code,{children:"sudo ls /"}),"."]}),"\n",(0,t.jsxs)(n.p,{children:["Since ",(0,t.jsx)(n.code,{children:"frontend-dev"})," did not contribute to the code running on the\n",(0,t.jsx)(n.code,{children:"backend"})," host (which has SSH listening on port 2223), they should not be able\nto login."]}),"\n",(0,t.jsx)(n.pre,{children:(0,t.jsx)(n.code,{className:"language-shell",children:"ssh -p 2223 frontend-dev@localhost \\\n -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null\n"})}),"\n",(0,t.jsx)(n.h3,{id:"5-elevate-a-users-rights-through-policy",children:"5. Elevate a user's rights through policy"}),"\n",(0,t.jsx)(n.p,{children:"Suppose you have a ticketing system for elevation, where you generate tickets for users\nthat need elevated rights, send the ticket to the user, and expire those tickets when\ntheir rights should be removed."}),"\n",(0,t.jsx)(n.p,{children:"Mock the current state of this simple ticketing system's API with some data."}),"\n",(0,t.jsx)(n.pre,{children:(0,t.jsx)(n.code,{className:"language-shell",children:'mkdir elevate\ncat <<EOF > elevate/data.json\n{\n "tickets": {\n "frontend-dev": "1234"\n }\n}\nEOF\n'})}),"\n",(0,t.jsxs)(n.p,{children:["This means that for now, if the ",(0,t.jsx)(n.code,{children:"frontend-dev"})," user can provide ticket number ",(0,t.jsx)(n.code,{children:"1234"}),",\nthey should be able to SSH into all servers."]}),"\n",(0,t.jsx)(n.p,{children:"Write policy to ensure that this happens."}),"\n",(0,t.jsx)(n.p,{children:"First, make the PAM module take input from the user."}),"\n",(0,t.jsxs)(n.p,{children:[(0,t.jsx)(n.strong,{children:"display.rego"}),":"]}),"\n",(0,t.jsx)(n.pre,{children:(0,t.jsx)(n.code,{className:"language-rego",children:'package display\n\n# What should be prompted to the user?\ndisplay_spec := [\n {\n "message": "Please enter an elevation ticket if you have one:",\n "style": "prompt_echo_on",\n "ke
1y": "ticket"\n }\n]\n'})}),"\n",(0,t.jsx)(n.p,{children:"Then make sure that the authorization takes this input into account."}),"\n",(0,t.jsxs)(n.p,{children:[(0,t.jsx)(n.strong,{children:"sudo_authz_elevated.rego"}),":"]}),"\n",(0,t.jsx)(n.pre,{children:(0,t.jsx)(n.code,{className:"language-rego",children:"# A package can be defined across multiple files.\npackage sudo.authz\n\nimport data.elevate\nimport input.display_responses\nimport input.sysinfo\n\n# Allow this user if the elevation ticket they provided matches our mock API\n# of an internal elevation system.\nallow if {\n elevate.tickets[sysinfo.pam_username] == display_responses.ticket\n}\n"})}),"\n",(0,t.jsx)(n.p,{children:"Now build a new bundle for OPA to use."}),"\n",(0,t.jsx)(n.pre,{children:(0,t.jsx)(n.code,{className:"language-shell",children:"opa build -b .\n"})}),"\n",(0,t.jsxs)(n.p,{children:["Confirm that the user ",(0,t.jsx)(n.code,{children:"frontend-dev"})," can indeed use ",(0,t.jsx)(n.code,{children:"sudo"}),"."]}),"\n",(0,t.jsx)(n.pre,{children:(0,t.jsx)(n.code,{className:"language-shell",children:"ssh -p 2222 frontend-dev@localhost \\\n -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null\n\nsudo ls /\n"})}),"\n",(0,t.jsxs)(n.p,{children:["You should be prompted with the message defined in the ",(0,t.jsx)(n.em,{children:"display"})," policy\nfor both the SSH and ",(0,t.jsx)(n.code,{children:"sudo"})," authorization cycles.\nThis happens because the ",(0,t.jsx)(n.em,{children:"display"})," policy is shared by the PAM configurations of SSH and ",(0,t.jsx)(n.code,{children:"sudo"}),".\nIn production, it is more practical to use separate policy packages for each PAM configuration."]}),"\n",(0,t.jsxs)(n.p,{children:["The SSH ",(0,t.jsx)(n.em,{children:"authz"})," policy has not been defined to work with elevation, so you can enter any value\ninto the prompt that comes up for SSH."]}),"\n",(0,t.jsxs)(n.p,{children:["For ",(0,t.jsx)(n.code,{children:"sudo"}),", enter the ticket number ",(0,t.jsx)(n.code,{children:"1234"})," to get access."]}),"\n",(0,t.jsx)(n.p,{children:"Lastly, update the mocked elevation API and confirm the user's original rights are restored."}),"\n",(0,t.jsx)(n.pre,{children:(0,t.jsx)(n.code,{className:"language-shell",children:'cat <<EOF > elevate/data.json\n{\n "tickets": {}\n}\nEOF\n'})}),"\n",(0,t.jsx)(n.p,{children:"Once again, build the bundle with this new data"}),"\n",(0,t.jsx)(n.pre,{children:(0,t.jsx)(n.code,{className:"language-bash",children:"opa build -b .\n"})}),"\n",(0,t.jsxs)(n.p,{children:["You will find that running ",(0,t.jsx)(n.code,{children:"sudo ls /"})," as the ",(0,t.jsx)(n.code,{children:"frontend-dev"})," user is disallowed again."]}),"\n",(0,t.jsxs)(n.p,{children:["It is possible to configure the ",(0,t.jsx)(n.em,{children:"display"})," policy to only make the PAM module prompt for the\nelevation ticket when the mock API has a non-empty ",(0,t.jsx)(n.code,{children:"tickets"})," object. So when there are no\nelevated users, there will be no prompt for a ticket. This can be done using the Rego\n",(0,t.jsxs)(n.a,{href:"./policy-reference/builtins/aggregates",children:[(0,t.jsx)(n.code,{children:"count"})," aggregate"]}),"."]}),"\n",(0,t.jsx)(n.h2,{id:"summary",children:"Summary"}),"\n",(0,t.jsx)(n.p,{children:"You learned a number of things about SSH with OPA:"}),"\n",(0,t.jsxs)(n.ul,{children:["\n",(0,t.jsxs)(n.li,{children:["OPA gives you fine-grained access control over SSH, ",(0,t.jsx)(n.code,{children:"sudo"}),", and any other application that uses PAM.\nAlthough this tutorial used the some of the same policies for both\nSSH and sudo, you should use separate, fine-grained policies for each application that supports PAM."]}),"\n",(0,t.jsx)(n.li,{children:"Writing allow/deny policies to control who has access to what using context from the user and host."}),"\n",(0,t.jsx)(n.li,{children:"Importing external data into OPA and writing policies that depend on that data."}),"\n"]}),"\n",(0,t.jsxs)(n.p,{children:["The code for the PAM module used in this tutorial can be found in the\n",(0,t.jsx)(n.a,{href:"https://github.com/open-policy-agent/contrib",children:"open-policy-agent/contrib"}),"\nrepository."]})]})}function h(e={}){const{wrapper:n}={...(0,i.R)(),...e.components};return n?(0,t.jsx)(n,{...e,children:(0,t.jsx)(c,{...e})}):c(e)}}}]);
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.