This is an excellent opportunity to see how to policy enable an existing service.

This tutorial helps you get started with OPA and introduces you to core concepts in OPA.

For the purpose of this tutorial, we want to use OPA to enforce a policy that prevents users from running insecure containers.

This tutorial illustrates two key concepts:

  1. OPA policy definition is decoupled from the implementation of the service (in this case Docker). The administrator is empowered to define and manage policies without requiring changes to any of the apps.

  2. Both the data relevant to policy and the policy definitions themselves can change rapidly.

This tutorial requires:

  • Docker Engine 18.06.0-ce or newer
  • Docker API version 1.38 or newer
  • or sudo access

The tutorial has been tested on the following platforms:

  • Ubuntu 18.04 (64-bit)

If you are using a different distro, OS, or architecture, the steps will be the same. However, there may be slight differences in the commands you need to run.

Several of the steps below require root or sudo access. When you are modifying files under /etc/docker or signalling the Docker daemon to restart, you will need root access.

  1. package docker.authz
  2. allow = true

This policy defines a single rule named allow that always produces the decision true. Once all of the components are running, we will come back to the policy.

2. Install the opa-docker-authz plugin.

  1. docker plugin install openpolicyagent/opa-docker-authz-v2:0.4 opa-args="-policy-file /opa/policies/authz.rego"

You need to configure the Docker daemon to use the plugin for authorization.

  1. cat > /etc/docker/daemon.json <<EOF
  2. {
  3. "authorization-plugins": ["openpolicyagent/opa-docker-authz-v2:0.4"]
  4. }
  5. EOF

Signal the Docker daemon to reload the configuration file.

  1. kill -HUP $(pidof dockerd)

4. Run a simple Docker command to make sure everything is still working.

  1. docker ps

If everything is setup correctly, the command should exit successfully. You can expect to see log messages from OPA and the plugin.

Let’s modify our policy to deny all requests:

/etc/docker/policies/authz.rego:

  1. package docker.authz
  2. allow = false

In OPA, rules defines the content of documents. Documents be boolean values (true/false) or they can represent more complex structures using arrays, objects, strings, etc.

In the example above we modified the policy to always return false so that requests will be rejected.

The output should be:

  1. Error response from daemon: authorization denied by plugin opa-docker-authz: request rejected by administrative policy

To learn more about how rules define the content of documents, see:

With this policy in place, users will not be able to run any Docker commands. Go ahead and try other commands such as docker run or docker pull. They will all be rejected.

6. Update the policy to reject requests with the unconfined profile:

/etc/docker/policies/authz.rego:

  1. package docker.authz
  2. default allow = false
  3. allow {
  4. not deny
  5. }
  6. deny {
  7. seccomp_unconfined
  8. }
  9. seccomp_unconfined {
  10. # This expression asserts that the string on the right-hand side is equal
  11. # to an element in the array SecurityOpt referenced on the left-hand side.
  12. input.Body.HostConfig.SecurityOpt[_] == "seccomp:unconfined"
  13. }

The plugin queries the allow rule to authorize requests to Docker. The input document is set to the attributes passed from Docker.

  1. allow
  1. {
  2. "AuthMethod": "",
  3. "Body": {
  4. "AttachStderr": true,
  5. "AttachStdin": false,
  6. "AttachStdout": true,
  7. "Cmd": null,
  8. "Domainname": "",
  9. "Entrypoint": null,
  10. "Env": [],
  11. "HostConfig": {
  12. "AutoRemove": false,
  13. "Binds": null,
  14. "BlkioDeviceReadBps": null,
  15. "BlkioDeviceReadIOps": null,
  16. "BlkioDeviceWriteIOps": null,
  17. "BlkioWeight": 0,
  18. "BlkioWeightDevice": [],
  19. "CapAdd": null,
  20. "CapDrop": null,
  21. "CgroupParent": "",
  22. "ConsoleSize": [
  23. 0,
  24. 0
  25. ],
  26. "ContainerIDFile": "",
  27. "CpuCount": 0,
  28. "CpuPercent": 0,
  29. "CpuPeriod": 0,
  30. "CpuQuota": 0,
  31. "CpuRealtimePeriod": 0,
  32. "CpuRealtimeRuntime": 0,
  33. "CpuShares": 0,
  34. "CpusetCpus": "",
  35. "CpusetMems": "",
  36. "DeviceCgroupRules": null,
  37. "Devices": [],
  38. "DiskQuota": 0,
  39. "Dns": [],
  40. "DnsOptions": [],
  41. "DnsSearch": [],
  42. "ExtraHosts": null,
  43. "GroupAdd": null,
  44. "IOMaximumBandwidth": 0,
  45. "IOMaximumIOps": 0,
  46. "IpcMode": "",
  47. "Isolation": "",
  48. "KernelMemory": 0,
  49. "Links": null,
  50. "LogConfig": {
  51. "Config": {},
  52. "Type": ""
  53. },
  54. "MaskedPaths": null,
  55. "Memory": 0,
  56. "MemoryReservation": 0,
  57. "MemorySwap": 0,
  58. "MemorySwappiness": -1,
  59. "NanoCpus": 0,
  60. "NetworkMode": "default",
  61. "OomKillDisable": false,
  62. "OomScoreAdj": 0,
  63. "PidMode": "",
  64. "PidsLimit": 0,
  65. "PortBindings": {},
  66. "Privileged": false,
  67. "PublishAllPorts": false,
  68. "ReadonlyPaths": null,
  69. "ReadonlyRootfs": false,
  70. "RestartPolicy": {
  71. "MaximumRetryCount": 0,
  72. "Name": "no"
  73. },
  74. "SecurityOpt": null,
  75. "ShmSize": 0,
  76. "UTSMode": "",
  77. "Ulimits": null,
  78. "UsernsMode": "",
  79. "VolumeDriver": "",
  80. "VolumesFrom": null
  81. "Hostname": "",
  82. "Image": "hello-world",
  83. "Labels": {},
  84. "NetworkingConfig": {
  85. },
  86. "OnBuild": null,
  87. "OpenStdin": false,
  88. "StdinOnce": false,
  89. "Tty": false,
  90. "User": "",
  91. "Volumes": {},
  92. "WorkingDir": ""
  93. },
  94. "Headers": {
  95. "Content-Length": "1470",
  96. "Content-Type": "application/json",
  97. "User-Agent": "Docker-Client/18.06.1-ce (linux)"
  98. },
  99. "Method": "POST",
  100. "Path": "/v1.38/containers/create",
  101. "User": ""
  102. }

For the input above, the value of allow is:

  1. true

7. Test the policy is working by running a simple container:

  1. docker run hello-world

Now try running the same container but disable seccomp (which should be prevented by the policy):

Congratulations! You have successfully prevented containers from running without seccomp!

The rest of the tutorial shows how you can grant fine grained access to specific clients.

  1. mkdir -p ~/.docker
  2. cp ~/.docker/config.json ~/.docker/config.json~

To identify the user, include an HTTP header in all of the requests sent to the Docker daemon:

  1. cat >~/.docker/config.json <<EOF
  2. {
  3. "HttpHeaders": {
  4. "Authz-User": "bob"
  5. }
  6. }
  7. EOF

9. Update the policy to include basic user access controls.

  1. package docker.authz
  2. default allow = false
  3. # allow if the user is granted read/write access.
  4. allow {
  5. user_id := input.Headers["Authz-User"]
  6. user := users[user_id]
  7. not user.readOnly
  8. }
  9. # allow if the user is granted read-only access and the request is a GET.
  10. allow {
  11. user_id := input.Headers["Authz-User"]
  12. users[user_id].readOnly
  13. input.Method == "GET"
  14. }
  15. # users defines permissions for the user. In this case, we define a single
  16. # attribute 'readOnly' that controls the kinds of commands the user can run.
  17. users = {
  18. "bob": {"readOnly": true},
  19. "alice": {"readOnly": false},
  20. }

10. Attempt to run a container.

Because the configured user is "bob", the request is rejected:

  1. docker run hello-world
  1. cat > ~/.docker/config.json <<EOF
  2. {
  3. "HttpHeaders": {
  4. "Authz-User": "alice"
  5. }
  6. }
  7. EOF

    That’s it!