When automating deployments with Jenkins, we often need to restart services after updating application files. The problem arises when Jenkins (running as a non-root user) needs to control systemd services that typically require root privileges.
In this scenario, we have:
- A Spring Boot application packaged as myapp.jar
- A systemd service unit at /etc/systemd/system/myapp.service
- A dedicated 'myapp' user owning the application files
- Jenkins configured to SSH as 'myapp' for deployments
The most secure approach is to create a Polkit rule that grants specific service control permissions to the 'myapp' user:
# Create a new polkit rule file
sudo nano /etc/polkit-1/rules.d/10-myapp.rules
Add this content:
polkit.addRule(function(action, subject) {
if (action.id == "org.freedesktop.systemd1.manage-units" &&
action.lookup("unit") == "myapp.service" &&
subject.user == "myapp") {
return polkit.Result.YES;
}
});
If Polkit isn't available, we can create a sudoers entry with precise permissions:
# Create a new sudoers file fragment
sudo nano /etc/sudoers.d/myapp-service-control
Add this content:
myapp ALL=(root) NOPASSWD: /bin/systemctl start myapp
myapp ALL=(root) NOPASSWD: /bin/systemctl stop myapp
myapp ALL=(root) NOPASSWD: /bin/systemctl restart myapp
Test the configuration by switching to the 'myapp' user:
sudo -u myapp systemctl restart myapp
If successful, Jenkins can now execute these commands via SSH without requiring root access.
Here's how to implement this in a Jenkins pipeline:
pipeline {
agent any
stages {
stage('Deploy') {
steps {
sshagent(['myapp-ssh-key']) {
sh '''
ssh myapp@myserver "sudo systemctl stop myapp"
scp target/myapp.jar myapp@myserver:/home/myapp/
ssh myapp@myserver "sudo systemctl start myapp"
'''
}
}
}
}
}
When implementing this solution:
- Always use dedicated service accounts (not shared with human users)
- Restrict SSH access to specific commands if possible
- Regularly audit service control permissions
- Consider using configuration management tools for more complex scenarios
When deploying Spring Boot applications as systemd services, a common security dilemma arises: how to enable CI/CD systems (like Jenkins) to manage service lifecycle operations without granting full sudo privileges. The standard approach of requiring root for systemctl
commands creates unnecessary security exposure.
Modern systemd versions (≥ 217) include granular permission controls through polkit policies. We can leverage this to allow specific users to manage defined services. Here's how to implement it for our myapp
scenario:
# Create a polkit rule file
sudo nano /etc/polkit-1/rules.d/10-myapp.rules
Add this JavaScript-based policy rule:
polkit.addRule(function(action, subject) {
if (action.id == "org.freedesktop.systemd1.manage-units" &&
action.lookup("unit") == "myapp.service" &&
subject.user == "myapp") {
return polkit.Result.YES;
}
});
For older systems without polkit support, we can create a granular sudo exception:
# Create a dedicated sudoers file
sudo visudo -f /etc/sudoers.d/myapp-management
# Add these specific permissions
myapp ALL=(root) NOPASSWD: /bin/systemctl start myapp
myapp ALL=(root) NOPASSWD: /bin/systemctl stop myapp
myapp ALL=(root) NOPASSWD: /bin/systemctl restart myapp
With permissions configured, here's how to implement the Jenkins pipeline:
pipeline {
agent any
stages {
stage('Deploy') {
steps {
sshagent(['myapp-ssh-key']) {
sh '''
ssh -o StrictHostKeyChecking=no myapp@myserver \
"sudo systemctl stop myapp && \
cp backend-1.0-SNAPSHOT.jar /home/myapp/ && \
sudo systemctl start myapp"
'''
}
}
}
}
}
- Always use dedicated service accounts (never reuse personal accounts)
- Regularly audit polkit/sudoers rules
- Consider implementing two-factor authentication for Jenkins deployments
- Use signed artifacts to prevent malicious jar replacement
Ensure your myapp.service
includes proper security directives:
[Unit]
Description=My Spring Boot Application
After=syslog.target
[Service]
User=myapp
Group=myapp
WorkingDirectory=/home/myapp
ExecStart=/usr/bin/java -jar /home/myapp/myapp.jar
Restart=on-failure
# Security hardening
PrivateTmp=true
ProtectSystem=full
NoNewPrivileges=true