May 31, 2026
How to secure D-Bus methods with polkit
Exposing a D-Bus method is only half of the problem. The other half is deciding who is allowed to call it.
In the previous post we created a small D-Bus service in C++ with GetStatus(), SetStatus(status) and a StatusChanged(status) signal. That service validated input, updated internal state and emitted a signal after the state changed.
But validation is not authorization. Checking that a string is not empty does not answer a more important question: is this caller allowed to change the device state at all?
This is where polkit fits. In this article we will take the same demo device idea, move it to the system bus and protect the state-changing method with a polkit action.
Note
This article is the next part of the D-Bus series. It builds on the previous posts about the D-Bus mental model, introspection and implementing a small service with sdbus-c++.
- How D-Bus works in Embedded Linux – start here if you want the mental model: buses, services, objects, interfaces, methods, properties and signals.
- D-Bus Introspection in Practice – read this if you want to inspect real D-Bus APIs from the command line.
- Implementing a small D-Bus service in C++ – read this if you want the service code this article builds on.

The problem polkit solves
A system daemon often runs with more privileges than its clients. For example, it may be able to restart services, update firmware, change network configuration or reboot a device. The client should not get those privileges directly. The client should ask the daemon to perform a specific operation.
That creates a privilege boundary:
- The client is usually untrusted.
- The D-Bus bus routes the message.
- The service owns the real operation.
- polkit helps the service decide whether the caller is allowed to perform that operation.
The important detail is that polkit is normally used by the privileged service, not by the client. A client can request SetStatus("ready"), but the service must still ask: is the sender of this method call authorized for this action?
Warning
Do not put the security decision in the client. A client can be modified, replaced or bypassed. The D-Bus service must enforce authorization at the method boundary.
D-Bus policy and polkit are not the same thing
Before we write code, it helps to separate two different layers that are often confused.
- D-Bus bus policy controls coarse access to the bus name and messages. It can say which user may own a service name and which clients may send messages to that destination.
- polkit controls higher-level actions. It can say whether this caller may perform this operation now, possibly after authenticating as an administrator.
For a real system service, you usually need both. D-Bus policy lets your daemon own com.gr8software.DemoDevice on the system bus. polkit decides whether the caller may execute a sensitive method such as SetStatus.
Warning
A permissive D-Bus policy is not a complete authorization model. If the bus policy allows clients to reach your daemon, your daemon still needs to check sensitive operations before doing work.

What we are going to protect
We will keep the same small demo device API, but treat the methods differently:
GetStatus()stays read-only and does not need authentication in this example.SetStatus(status)changes service state, so it will require a polkit authorization check.StatusChanged(status)stays a signal emitted by the service after the state changes.
The protected operation will be represented by this polkit action ID:
com.gr8software.demodevice.set-status
The action ID is intentionally lowercase. D-Bus interface and method names often use CamelCase, but polkit action names are usually written as lowercase namespaced identifiers. Keeping this distinction visible makes the code easier to read.
Project layout
For this version, the demo needs a little more than the previous session-bus example:
dbus-demo-service-polkit/
|-- CMakeLists.txt
|-- main.cpp
|-- com.gr8software.DemoDevice.conf
`-- com.gr8software.demodevice.policy
The C++ service performs the authorization check. The .conf file is the system bus policy. The .policy file declares the polkit action.
Declare the polkit action
Create com.gr8software.demodevice.policy:
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE policyconfig PUBLIC
"-//freedesktop//DTD polkit Policy Configuration 1.0//EN"
"http://www.freedesktop.org/software/polkit/policyconfig-1.dtd">
<policyconfig>
<vendor>gr8Software</vendor>
<vendor_url>https://gr8software.pl</vendor_url>
<action id="com.gr8software.demodevice.set-status">
<description>Change demo device status</description>
<message>Authentication is required to change the demo device status</message>
<defaults>
<allow_any>no</allow_any>
<allow_inactive>auth_admin</allow_inactive>
<allow_active>auth_admin</allow_active>
</defaults>
</action>
</policyconfig>
This file does not protect anything by itself. It only declares an action and its default authorization behavior. The service still has to call polkit when SetStatus is invoked.
The defaults mean:
allow_anyisno, so random non-session callers are not implicitly authorized.allow_inactiveisauth_admin, so an inactive local session must authenticate as an administrator.allow_activeisauth_admin, so an active local session must also authenticate as an administrator.
Tip
For less sensitive repeated actions you may see auth_admin_keep. Be careful with it. It keeps authorization for a short time, so it is not a good default when the decision depends on changing arguments or device state.
Install the action file:
sudo install -m 0644 com.gr8software.demodevice.policy \
/usr/share/polkit-1/actions/com.gr8software.demodevice.policy
Then check that polkit can see it:
pkaction --verbose | grep -A 8 com.gr8software.demodevice.set-status
Add a system bus policy
Because this example now runs on the system bus, the daemon also needs a D-Bus system bus policy file.
Create com.gr8software.DemoDevice.conf:
<!DOCTYPE busconfig PUBLIC
"-//freedesktop//DTD D-BUS Bus Configuration 1.0//EN"
"http://www.freedesktop.org/standards/dbus/1.0/busconfig.dtd">
<busconfig>
<policy user="root">
<allow own="com.gr8software.DemoDevice"/>
</policy>
<policy context="default">
<allow send_destination="com.gr8software.DemoDevice"/>
</policy>
</busconfig>
Install it under the system bus configuration directory. On many distributions this is:
sudo install -m 0644 com.gr8software.DemoDevice.conf \
/etc/dbus-1/system.d/com.gr8software.DemoDevice.conf
sudo systemctl reload dbus || sudo systemctl restart dbus
Warning
Restarting the system bus can disrupt services on some systems. On a development machine this is usually acceptable. On a product, handle bus policy updates as part of the image or package installation process.
This policy is intentionally simple for learning. It lets the root-owned daemon own the service name and lets clients send messages to it. The actual decision for SetStatus is still made inside the service with polkit.
Update CMake
The service now links against both sdbus-c++ and libpolkit-gobject-1. One simple way to find polkit from CMake is through pkg-config.
Create CMakeLists.txt:
cmake_minimum_required(VERSION 3.16)
project(dbus_demo_service_polkit LANGUAGES CXX)
set(CMAKE_CXX_STANDARD 17)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
set(CMAKE_CXX_EXTENSIONS OFF)
find_package(PkgConfig REQUIRED)
find_package(sdbus-c++ 2.0 REQUIRED)
pkg_check_modules(POLKIT_GOBJECT REQUIRED IMPORTED_TARGET polkit-gobject-1)
add_executable(dbus-demo-service main.cpp)
target_link_libraries(dbus-demo-service
PRIVATE
SDBusCpp::sdbus-c++
PkgConfig::POLKIT_GOBJECT
)
On Debian or Ubuntu, the development packages are usually:
sudo apt install -y \
libpolkit-gobject-1-dev \
pkg-config
Ask polkit from the method handler
The key piece is the caller identity. For a D-Bus method call on the system bus, the service can obtain the sender’s unique bus name, for example :1.42. That unique name becomes the polkit subject.
Then the service asks the polkit authority whether that subject is authorized for com.gr8software.demodevice.set-status.
Warning
Do not authorize based only on a client-provided argument such as a username, PID string or application name. The service must use identity information from the bus message or from trusted bus credentials, not from data supplied by the caller.

Create main.cpp:
#include <sdbus-c++/sdbus-c++.h>
#include <polkit/polkit.h>
#include <iostream>
#include <stdexcept>
#include <string>
namespace
{
constexpr const char* SERVICE_NAME = "com.gr8software.DemoDevice";
constexpr const char* OBJECT_PATH = "/com/gr8software/DemoDevice";
constexpr const char* INTERFACE_NAME = "com.gr8software.DemoDevice";
constexpr const char* SET_STATUS_ACTION =
"com.gr8software.demodevice.set-status";
std::string takeGErrorMessage(GError* error)
{
if (error == nullptr)
{
return "unknown error";
}
std::string message = error->message;
g_error_free(error);
return message;
}
class PolkitAuthorizer
{
public:
PolkitAuthorizer()
{
GError* error = nullptr;
authority_ = polkit_authority_get_sync(nullptr, &error);
if (authority_ == nullptr)
{
throw std::runtime_error(
"Failed to connect to polkit authority: " +
takeGErrorMessage(error));
}
}
~PolkitAuthorizer()
{
if (authority_ != nullptr)
{
g_object_unref(authority_);
}
}
PolkitAuthorizer(const PolkitAuthorizer&) = delete;
PolkitAuthorizer& operator=(const PolkitAuthorizer&) = delete;
void checkSystemBusCaller(const std::string& sender,
const char* actionId) const
{
PolkitSubject* subject =
polkit_system_bus_name_new(sender.c_str());
GError* error = nullptr;
PolkitAuthorizationResult* result =
polkit_authority_check_authorization_sync(
authority_,
subject,
actionId,
nullptr,
POLKIT_CHECK_AUTHORIZATION_FLAGS_ALLOW_USER_INTERACTION,
nullptr,
&error);
g_object_unref(subject);
if (error != nullptr)
{
throw sdbus::Error(
sdbus::Error::Name{
"com.gr8software.DemoDevice.Error.AuthorizationCheckFailed"},
takeGErrorMessage(error));
}
const bool authorized =
result != nullptr &&
polkit_authorization_result_get_is_authorized(result);
if (result != nullptr)
{
g_object_unref(result);
}
if (!authorized)
{
throw sdbus::Error(
sdbus::Error::Name{"org.freedesktop.DBus.Error.AccessDenied"},
"Not authorized to change the demo device status");
}
}
private:
PolkitAuthority* authority_{nullptr};
};
}
int main()
{
try
{
std::string status = "idle";
PolkitAuthorizer authorizer;
auto connection = sdbus::createSystemBusConnection(
sdbus::ServiceName{SERVICE_NAME});
auto object = sdbus::createObject(
*connection,
sdbus::ObjectPath{OBJECT_PATH});
object->addVTable(
sdbus::registerMethod("GetStatus")
.implementedAs([&status]() {
return status;
}),
sdbus::registerMethod("SetStatus")
.implementedAs([&status, &object, &authorizer]
(const std::string& newStatus) {
const auto message =
object->getCurrentlyProcessedMessage();
if (message == nullptr || message->getSender() == nullptr)
{
throw sdbus::Error(
sdbus::Error::Name{
"com.gr8software.DemoDevice.Error.UnknownCaller"},
"Cannot identify D-Bus caller");
}
const std::string sender = message->getSender();
authorizer.checkSystemBusCaller(
sender,
SET_STATUS_ACTION);
if (newStatus.empty())
{
throw sdbus::Error(
sdbus::Error::Name{
"com.gr8software.DemoDevice.Error.InvalidStatus"},
"Status must not be empty");
}
status = newStatus;
object->emitSignal("StatusChanged")
.onInterface(INTERFACE_NAME)
.withArguments(status);
}),
sdbus::registerSignal("StatusChanged")
.withParameters<std::string>("status"))
.forInterface(sdbus::InterfaceName{INTERFACE_NAME});
std::cout << "Service is running on the system bus\n";
std::cout << "Service name: " << SERVICE_NAME << '\n';
std::cout << "Object path: " << OBJECT_PATH << '\n';
connection->enterEventLoop();
}
catch (const sdbus::Error& error)
{
std::cerr << "D-Bus error: "
<< error.getName()
<< " - "
<< error.getMessage()
<< '\n';
return 1;
}
catch (const std::exception& error)
{
std::cerr << "Error: " << error.what() << '\n';
return 1;
}
return 0;
}
The important part is not the demo status string. The important part is the order inside SetStatus:
- Read the sender from the current D-Bus method call.
- Ask polkit whether that sender is authorized for the action.
- Reject the call before changing state if authorization fails.
- Validate the input.
- Update state.
- Emit the signal.
The authorization check happens before the state change. That is the boundary you want in real services too.
Tip
Use one polkit action per meaningful privileged operation. Do not use one generic action such as com.example.admin for everything. Fine-grained actions are easier to audit and easier for integrators to customize.
Build and run the service
Build the project:
cmake -S . -B build
cmake --build build
Run the service as root, because in this example the system bus policy allows root to own the service name:
sudo ./build/dbus-demo-service
You should see:
Service is running on the system bus
Service name: com.gr8software.DemoDevice
Object path: /com/gr8software/DemoDevice
Introspect the service
From another terminal, inspect the object on the system bus:
busctl --system introspect \
com.gr8software.DemoDevice \
/com/gr8software/DemoDevice
You should still see the public API:
com.gr8software.DemoDevice interface - -
.GetStatus method - s
.SetStatus method s -
.StatusChanged signal s -
polkit does not usually hide methods from introspection. The method is visible, but calling it successfully still requires authorization.
Call the read-only method
First call GetStatus. This method does not change state, so our example does not protect it with polkit:
busctl --system call \
com.gr8software.DemoDevice \
/com/gr8software/DemoDevice \
com.gr8software.DemoDevice \
GetStatus
Expected result:
s "idle"
Call the protected method
Now call SetStatus:
busctl --system call \
com.gr8software.DemoDevice \
/com/gr8software/DemoDevice \
com.gr8software.DemoDevice \
SetStatus \
s "ready"
If the caller is authorized, the method returns successfully and the service emits StatusChanged("ready"). If the caller is not authorized, the service returns a D-Bus error instead of changing state.
For example:
Call failed: Not authorized to change the demo device status
Note
On a desktop system, an authentication agent may show a graphical prompt. On a headless device or SSH session, there may be no agent available. In that case, the authorization check may fail unless you register a text agent or test with a caller that is already allowed by policy.
For command-line testing over SSH, you can use pkttyagent in another terminal:
pkttyagent --process $$
Then repeat the protected method call from the shell associated with that session.

Allow a product-specific group
The .policy file defines default behavior. Product-specific authorization is usually placed in polkit rules under /etc/polkit-1/rules.d/.
For example, suppose your device has a Linux group called gr8-device, and members of that group should be allowed to change the demo status without typing an administrator password. A system integrator could add:
// /etc/polkit-1/rules.d/50-gr8-demo-device.rules
polkit.addRule(function(action, subject) {
if (action.id == "com.gr8software.demodevice.set-status" &&
subject.isInGroup("gr8-device")) {
return polkit.Result.YES;
}
return polkit.Result.NOT_HANDLED;
});
This rule is not part of the daemon code. That is the point. Your service enforces the action check. The product or site decides who is allowed for that action.
Warning
Applications should normally install action definitions, not site-specific authorization rules. Rules are for system administrators, distributions or product images that decide local policy.
Use separate actions for different risk levels
A common mistake is protecting everything with one action. That makes policy too coarse.
Imagine a real device interface:
com.gr8software.device.read-diagnosticscom.gr8software.device.set-modecom.gr8software.device.install-updatecom.gr8software.device.factory-resetcom.gr8software.device.reboot
These operations do not have the same risk. Reading diagnostics may be safe for a support group. Installing an update may require an administrator. Factory reset may require a stricter rule or a physical-service mode.
Design polkit actions around operations users understand, not around C++ function names.
What about method arguments
In this demo, authorization depends only on the operation: changing status. In real services, the argument may matter.
For example, SetMode("maintenance") may be less sensitive than SetMode("factory"). You have two common options:
- Use separate D-Bus methods or separate polkit actions for operations with clearly different risk.
- Pass carefully selected details to polkit and let rules inspect those details.
For embedded products, separate actions are often easier to audit. They also produce clearer authentication messages for the user.
Warning
If a rule depends on method arguments or other dynamic details, avoid auth_admin_keep unless you are very sure it is safe. Kept authorizations can allow later checks for the same action and subject without re-evaluating the same context in the way you might expect.
Common mistakes
When adding polkit to a D-Bus service, these mistakes are easy to make:
- Checking authorization in the client. The client is not trusted. The service must enforce the decision.
- Calling polkit after doing the work. The authorization check must happen before any state change, file write, hardware access or privileged operation.
- Relying only on D-Bus policy. Bus policy is useful, but it is usually too coarse for method-level decisions.
- Using one action for everything. Fine-grained actions are easier to reason about and easier to customize.
- Forgetting headless systems. If there is no authentication agent, interactive authorization may fail. Test over SSH, serial console and your real product UI.
- Trusting caller-provided identity. Use the sender information from the D-Bus message or trusted bus credentials, not a username string passed as a method argument.
- Returning success after denial. Unauthorized calls should return a D-Bus error and should not partially apply the operation.
A practical design checklist
For a real embedded D-Bus service, I would use this checklist before shipping:
- List every method that changes system state.
- Decide which methods are read-only and which are privileged.
- Create one polkit action per meaningful privileged operation.
- Use clear action descriptions and authentication messages.
- Install a restrictive default
.policyfile. - Keep product-specific authorization in rules or image configuration.
- Check authorization inside the service before doing work.
- Return a D-Bus error when authorization fails.
- Test with an authorized user, unauthorized user, SSH session and no authentication agent.
- Review file paths, arguments and race conditions separately from polkit.
The last point matters. polkit answers who may ask for this operation. It does not automatically make the operation itself safe. Your service must still validate arguments, avoid unsafe file handling and protect against confused-deputy bugs.
The key idea
If you remember only one thing, remember this:
Tip
D-Bus delivers the method call, but your service owns the authorization decision. For privileged methods, check the caller with polkit before changing state.
The pattern is small: define a polkit action, let the system bus deliver the method call, extract the trusted caller identity, ask polkit, and reject unauthorized calls before touching the device.
Once that boundary is in place, your D-Bus API starts to look like a real system API: discoverable, scriptable, inspectable and protected by policy that can be adapted to the product.