Authorize Pacemaker actions with SO_PEERCRED

This commit is contained in:
Lyncoln
2026-09-17 12:45:46 -04:00
parent 773e046d42
commit 5a128fea36
4 changed files with 321 additions and 14 deletions
+120 -4
View File
@@ -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,
+2
View File
@@ -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
+175
View File
@@ -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")
+24 -10
View File
@@ -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, {
"resource": "postgresql-ha", "protocol_version": 1,
"action": "monitor", "resource": "postgresql-ha",
}) "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, {
"resource": "different-resource", "protocol_version": 1,
"action": "monitor", "resource": "different-resource",
}) "action": "monitor",
},
authorized_peer,
)
require( require(
response["ok"] is False, response["ok"] is False,