Authorize Pacemaker actions with SO_PEERCRED
This commit is contained in:
+120
-4
@@ -4,6 +4,7 @@ import json
|
|||||||
import os
|
import os
|
||||||
import signal
|
import signal
|
||||||
import socket
|
import socket
|
||||||
|
import struct
|
||||||
import traceback
|
import traceback
|
||||||
|
|
||||||
from registry import Registry, RegistryError
|
from registry import Registry, RegistryError
|
||||||
@@ -17,6 +18,8 @@ SOCKET_PATH = os.environ.get(
|
|||||||
server_socket = None
|
server_socket = None
|
||||||
registry = None
|
registry = None
|
||||||
pacemaker_resource_id = None
|
pacemaker_resource_id = None
|
||||||
|
pacemaker_peer_uid = None
|
||||||
|
pacemaker_peer_gid = None
|
||||||
|
|
||||||
PROTOCOL_VERSION = 1
|
PROTOCOL_VERSION = 1
|
||||||
|
|
||||||
@@ -65,7 +68,66 @@ def load_pacemaker_resource_binding(
|
|||||||
return resource_id
|
return resource_id
|
||||||
|
|
||||||
|
|
||||||
def handle_request(req):
|
def load_pacemaker_peer_authorization(
|
||||||
|
environ=None,
|
||||||
|
):
|
||||||
|
"""
|
||||||
|
Load the peer credentials that authorize Pacemaker resource actions.
|
||||||
|
|
||||||
|
In the rootless keep-id user namespace, host root is intentionally
|
||||||
|
unmapped and therefore appears as the configured overflow UID/GID.
|
||||||
|
Socket path permissions remain part of this authorization boundary.
|
||||||
|
"""
|
||||||
|
if environ is None:
|
||||||
|
environ = os.environ
|
||||||
|
|
||||||
|
raw_uid = environ.get("PACEMAKER_PEER_UID")
|
||||||
|
raw_gid = environ.get("PACEMAKER_PEER_GID")
|
||||||
|
|
||||||
|
if raw_uid is None or raw_gid is None:
|
||||||
|
raise RegistryError(
|
||||||
|
"PACEMAKER_PEER_UID and "
|
||||||
|
"PACEMAKER_PEER_GID must both be configured"
|
||||||
|
)
|
||||||
|
|
||||||
|
try:
|
||||||
|
peer_uid = int(raw_uid)
|
||||||
|
peer_gid = int(raw_gid)
|
||||||
|
|
||||||
|
except ValueError as exc:
|
||||||
|
raise RegistryError(
|
||||||
|
"PACEMAKER_PEER_UID and "
|
||||||
|
"PACEMAKER_PEER_GID must be integers"
|
||||||
|
) from exc
|
||||||
|
|
||||||
|
if peer_uid < 0 or peer_gid < 0:
|
||||||
|
raise RegistryError(
|
||||||
|
"PACEMAKER peer credentials must be non-negative"
|
||||||
|
)
|
||||||
|
|
||||||
|
return peer_uid, peer_gid
|
||||||
|
|
||||||
|
|
||||||
|
def get_peer_credentials(conn):
|
||||||
|
raw = conn.getsockopt(
|
||||||
|
socket.SOL_SOCKET,
|
||||||
|
socket.SO_PEERCRED,
|
||||||
|
struct.calcsize("3i"),
|
||||||
|
)
|
||||||
|
|
||||||
|
pid, uid, gid = struct.unpack("3i", raw)
|
||||||
|
|
||||||
|
return {
|
||||||
|
"pid": pid,
|
||||||
|
"uid": uid,
|
||||||
|
"gid": gid,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def handle_request(
|
||||||
|
req,
|
||||||
|
peer_credentials=None,
|
||||||
|
):
|
||||||
if not isinstance(req, dict):
|
if not isinstance(req, dict):
|
||||||
return protocol_response({
|
return protocol_response({
|
||||||
"ok": False,
|
"ok": False,
|
||||||
@@ -133,6 +195,35 @@ def handle_request(req):
|
|||||||
"action": action,
|
"action": action,
|
||||||
})
|
})
|
||||||
|
|
||||||
|
if peer_credentials is None:
|
||||||
|
return protocol_response({
|
||||||
|
"ok": False,
|
||||||
|
"error_code": "peer_credentials_unavailable",
|
||||||
|
"error": (
|
||||||
|
"peer credentials are required for "
|
||||||
|
"Pacemaker resource actions"
|
||||||
|
),
|
||||||
|
"resource": resource,
|
||||||
|
"action": action,
|
||||||
|
})
|
||||||
|
|
||||||
|
if (
|
||||||
|
peer_credentials.get("uid")
|
||||||
|
!= pacemaker_peer_uid
|
||||||
|
or peer_credentials.get("gid")
|
||||||
|
!= pacemaker_peer_gid
|
||||||
|
):
|
||||||
|
return protocol_response({
|
||||||
|
"ok": False,
|
||||||
|
"error_code": "peer_credential_mismatch",
|
||||||
|
"error": (
|
||||||
|
"peer credentials are not authorized "
|
||||||
|
"for Pacemaker resource actions"
|
||||||
|
),
|
||||||
|
"resource": resource,
|
||||||
|
"action": action,
|
||||||
|
})
|
||||||
|
|
||||||
return protocol_response(
|
return protocol_response(
|
||||||
registry.execute(
|
registry.execute(
|
||||||
resource,
|
resource,
|
||||||
@@ -170,16 +261,27 @@ def serve():
|
|||||||
global server_socket
|
global server_socket
|
||||||
global registry
|
global registry
|
||||||
global pacemaker_resource_id
|
global pacemaker_resource_id
|
||||||
|
global pacemaker_peer_uid
|
||||||
|
global pacemaker_peer_gid
|
||||||
|
|
||||||
registry = Registry()
|
registry = Registry()
|
||||||
pacemaker_resource_id = (
|
pacemaker_resource_id = (
|
||||||
load_pacemaker_resource_binding(registry)
|
load_pacemaker_resource_binding(registry)
|
||||||
)
|
)
|
||||||
|
(
|
||||||
|
pacemaker_peer_uid,
|
||||||
|
pacemaker_peer_gid,
|
||||||
|
) = load_pacemaker_peer_authorization()
|
||||||
|
|
||||||
|
socket_directory = os.path.dirname(
|
||||||
|
SOCKET_PATH
|
||||||
|
)
|
||||||
|
|
||||||
os.makedirs(
|
os.makedirs(
|
||||||
os.path.dirname(SOCKET_PATH),
|
socket_directory,
|
||||||
exist_ok=True,
|
exist_ok=True,
|
||||||
)
|
)
|
||||||
|
os.chmod(socket_directory, 0o700)
|
||||||
|
|
||||||
cleanup_socket()
|
cleanup_socket()
|
||||||
|
|
||||||
@@ -192,7 +294,7 @@ def serve():
|
|||||||
)
|
)
|
||||||
|
|
||||||
server_socket.bind(SOCKET_PATH)
|
server_socket.bind(SOCKET_PATH)
|
||||||
os.chmod(SOCKET_PATH, 0o660)
|
os.chmod(SOCKET_PATH, 0o600)
|
||||||
server_socket.listen(16)
|
server_socket.listen(16)
|
||||||
|
|
||||||
print(
|
print(
|
||||||
@@ -212,12 +314,23 @@ def serve():
|
|||||||
flush=True,
|
flush=True,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
print(
|
||||||
|
"Pacemaker peer authorization: "
|
||||||
|
f"uid={pacemaker_peer_uid} "
|
||||||
|
f"gid={pacemaker_peer_gid}",
|
||||||
|
flush=True,
|
||||||
|
)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
while True:
|
while True:
|
||||||
conn, _ = server_socket.accept()
|
conn, _ = server_socket.accept()
|
||||||
|
|
||||||
with conn:
|
with conn:
|
||||||
try:
|
try:
|
||||||
|
peer_credentials = (
|
||||||
|
get_peer_credentials(conn)
|
||||||
|
)
|
||||||
|
|
||||||
raw = b""
|
raw = b""
|
||||||
|
|
||||||
while b"\n" not in raw:
|
while b"\n" not in raw:
|
||||||
@@ -232,7 +345,10 @@ def serve():
|
|||||||
raw.decode("utf-8").strip()
|
raw.decode("utf-8").strip()
|
||||||
)
|
)
|
||||||
|
|
||||||
response = handle_request(req)
|
response = handle_request(
|
||||||
|
req,
|
||||||
|
peer_credentials,
|
||||||
|
)
|
||||||
|
|
||||||
except (
|
except (
|
||||||
RegistryError,
|
RegistryError,
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
RESOURCE_ID=postgresql-ha
|
RESOURCE_ID=postgresql-ha
|
||||||
PACEMAKER_RESOURCE_ID=postgresql-ha
|
PACEMAKER_RESOURCE_ID=postgresql-ha
|
||||||
|
PACEMAKER_PEER_UID=65534
|
||||||
|
PACEMAKER_PEER_GID=65534
|
||||||
PLUGIN=postgresql
|
PLUGIN=postgresql
|
||||||
|
|
||||||
MANAGED_CONTAINERD_SOCKET=/run/managed-containerd/managed-containerd.sock
|
MANAGED_CONTAINERD_SOCKET=/run/managed-containerd/managed-containerd.sock
|
||||||
|
|||||||
Executable
+175
@@ -0,0 +1,175 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
|
||||||
|
import os
|
||||||
|
import socket
|
||||||
|
import sys
|
||||||
|
|
||||||
|
sys.path.insert(0, "daemon")
|
||||||
|
|
||||||
|
import server
|
||||||
|
from registry import RegistryError
|
||||||
|
|
||||||
|
|
||||||
|
class FakeRegistry:
|
||||||
|
def __init__(self):
|
||||||
|
self.resources = {
|
||||||
|
"postgresql-ha": object(),
|
||||||
|
}
|
||||||
|
self.executions = []
|
||||||
|
|
||||||
|
def execute(self, resource, action):
|
||||||
|
self.executions.append((resource, action))
|
||||||
|
|
||||||
|
return {
|
||||||
|
"ok": True,
|
||||||
|
"resource": resource,
|
||||||
|
"plugin": "postgresql",
|
||||||
|
"action": action,
|
||||||
|
"result": {"accepted": True},
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def require(condition, message):
|
||||||
|
if not condition:
|
||||||
|
raise SystemExit("FAIL: " + message)
|
||||||
|
|
||||||
|
|
||||||
|
def require_registry_error(callback, message):
|
||||||
|
try:
|
||||||
|
callback()
|
||||||
|
except RegistryError:
|
||||||
|
return
|
||||||
|
|
||||||
|
raise SystemExit("FAIL: " + message)
|
||||||
|
|
||||||
|
|
||||||
|
print("===== SO_PEERCRED AUTHORIZATION =====")
|
||||||
|
|
||||||
|
require_registry_error(
|
||||||
|
lambda: server.load_pacemaker_peer_authorization({}),
|
||||||
|
"missing peer configuration was accepted",
|
||||||
|
)
|
||||||
|
print("missing_configuration rejected")
|
||||||
|
|
||||||
|
require_registry_error(
|
||||||
|
lambda: server.load_pacemaker_peer_authorization({
|
||||||
|
"PACEMAKER_PEER_UID": "invalid",
|
||||||
|
"PACEMAKER_PEER_GID": "65534",
|
||||||
|
}),
|
||||||
|
"invalid peer configuration was accepted",
|
||||||
|
)
|
||||||
|
print("invalid_configuration rejected")
|
||||||
|
|
||||||
|
peer_uid, peer_gid = (
|
||||||
|
server.load_pacemaker_peer_authorization({
|
||||||
|
"PACEMAKER_PEER_UID": "65534",
|
||||||
|
"PACEMAKER_PEER_GID": "65534",
|
||||||
|
})
|
||||||
|
)
|
||||||
|
|
||||||
|
require(
|
||||||
|
(peer_uid, peer_gid) == (65534, 65534),
|
||||||
|
"valid peer configuration was not loaded",
|
||||||
|
)
|
||||||
|
print("valid_configuration accepted")
|
||||||
|
|
||||||
|
left, right = socket.socketpair()
|
||||||
|
|
||||||
|
try:
|
||||||
|
observed = server.get_peer_credentials(left)
|
||||||
|
finally:
|
||||||
|
left.close()
|
||||||
|
right.close()
|
||||||
|
|
||||||
|
require(
|
||||||
|
observed["uid"] == os.geteuid(),
|
||||||
|
"SO_PEERCRED UID parsing is incorrect",
|
||||||
|
)
|
||||||
|
require(
|
||||||
|
observed["gid"] == os.getegid(),
|
||||||
|
"SO_PEERCRED GID parsing is incorrect",
|
||||||
|
)
|
||||||
|
require(
|
||||||
|
isinstance(observed["pid"], int),
|
||||||
|
"SO_PEERCRED PID parsing is incorrect",
|
||||||
|
)
|
||||||
|
print("kernel_credentials parsed")
|
||||||
|
|
||||||
|
registry = FakeRegistry()
|
||||||
|
|
||||||
|
server.registry = registry
|
||||||
|
server.pacemaker_resource_id = "postgresql-ha"
|
||||||
|
server.pacemaker_peer_uid = peer_uid
|
||||||
|
server.pacemaker_peer_gid = peer_gid
|
||||||
|
|
||||||
|
request = {
|
||||||
|
"protocol_version": 1,
|
||||||
|
"resource": "postgresql-ha",
|
||||||
|
"action": "monitor",
|
||||||
|
}
|
||||||
|
|
||||||
|
response = server.handle_request(
|
||||||
|
request,
|
||||||
|
{
|
||||||
|
"pid": 0,
|
||||||
|
"uid": 65534,
|
||||||
|
"gid": 65534,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
require(
|
||||||
|
response["ok"] is True,
|
||||||
|
"authorized root peer was rejected",
|
||||||
|
)
|
||||||
|
require(
|
||||||
|
registry.executions
|
||||||
|
== [("postgresql-ha", "monitor")],
|
||||||
|
"authorized request did not execute exactly once",
|
||||||
|
)
|
||||||
|
print("authorized_root_peer executed")
|
||||||
|
|
||||||
|
response = server.handle_request(
|
||||||
|
request,
|
||||||
|
{
|
||||||
|
"pid": os.getpid(),
|
||||||
|
"uid": 1000,
|
||||||
|
"gid": 1000,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
require(
|
||||||
|
response["ok"] is False,
|
||||||
|
"David peer was authorized",
|
||||||
|
)
|
||||||
|
require(
|
||||||
|
response["error_code"]
|
||||||
|
== "peer_credential_mismatch",
|
||||||
|
"David rejection has the wrong classification",
|
||||||
|
)
|
||||||
|
require(
|
||||||
|
registry.executions
|
||||||
|
== [("postgresql-ha", "monitor")],
|
||||||
|
"unauthorized request reached the registry",
|
||||||
|
)
|
||||||
|
print("david_peer rejected")
|
||||||
|
|
||||||
|
response = server.handle_request(request)
|
||||||
|
|
||||||
|
require(
|
||||||
|
response["ok"] is False,
|
||||||
|
"credential-free request was authorized",
|
||||||
|
)
|
||||||
|
require(
|
||||||
|
response["error_code"]
|
||||||
|
== "peer_credentials_unavailable",
|
||||||
|
"missing credentials have the wrong classification",
|
||||||
|
)
|
||||||
|
require(
|
||||||
|
registry.executions
|
||||||
|
== [("postgresql-ha", "monitor")],
|
||||||
|
"credential-free request reached the registry",
|
||||||
|
)
|
||||||
|
print("missing_credentials rejected")
|
||||||
|
|
||||||
|
print()
|
||||||
|
print("PEER_AUTHORIZATION_TEST_PASS=1")
|
||||||
@@ -95,6 +95,14 @@ print("valid_binding accepted")
|
|||||||
|
|
||||||
server.registry = registry
|
server.registry = registry
|
||||||
server.pacemaker_resource_id = binding
|
server.pacemaker_resource_id = binding
|
||||||
|
server.pacemaker_peer_uid = 65534
|
||||||
|
server.pacemaker_peer_gid = 65534
|
||||||
|
|
||||||
|
authorized_peer = {
|
||||||
|
"pid": 1234,
|
||||||
|
"uid": 65534,
|
||||||
|
"gid": 65534,
|
||||||
|
}
|
||||||
|
|
||||||
ping = server.handle_request({
|
ping = server.handle_request({
|
||||||
"protocol_version": 1,
|
"protocol_version": 1,
|
||||||
@@ -113,11 +121,14 @@ require(
|
|||||||
|
|
||||||
print("ping_binding reported")
|
print("ping_binding reported")
|
||||||
|
|
||||||
response = server.handle_request({
|
response = server.handle_request(
|
||||||
|
{
|
||||||
"protocol_version": 1,
|
"protocol_version": 1,
|
||||||
"resource": "postgresql-ha",
|
"resource": "postgresql-ha",
|
||||||
"action": "monitor",
|
"action": "monitor",
|
||||||
})
|
},
|
||||||
|
authorized_peer,
|
||||||
|
)
|
||||||
|
|
||||||
require(
|
require(
|
||||||
response["ok"] is True,
|
response["ok"] is True,
|
||||||
@@ -131,11 +142,14 @@ require(
|
|||||||
|
|
||||||
print("matching_resource executed")
|
print("matching_resource executed")
|
||||||
|
|
||||||
response = server.handle_request({
|
response = server.handle_request(
|
||||||
|
{
|
||||||
"protocol_version": 1,
|
"protocol_version": 1,
|
||||||
"resource": "different-resource",
|
"resource": "different-resource",
|
||||||
"action": "monitor",
|
"action": "monitor",
|
||||||
})
|
},
|
||||||
|
authorized_peer,
|
||||||
|
)
|
||||||
|
|
||||||
require(
|
require(
|
||||||
response["ok"] is False,
|
response["ok"] is False,
|
||||||
|
|||||||
Reference in New Issue
Block a user