DEV Community

Cover image for Day 73: Six Permissions and a Repeat
Nnamdi Felix Ibe
Nnamdi Felix Ibe

Posted on AI-assisted

Day 73: Six Permissions and a Repeat

Day 73 of DevOps, Day 23 of Azure. Both tasks today looked uniform and were not.

Six permissions in one list turned out to come from three different places in Jenkins. And an Azure deployment I had run the day before, with the same script and the same image, behaved differently enough to change which verification I would trust.

One Jenkins task, one Azure task. Give two users different permissions on one job, then repeat yesterday's self-configuring VM from the CLI. The tasks come from the KodeKloud Engineer platform.

Task 1: Two users, one job, different access

The task: grant Rohan build, cancel, configure, read, update and tag on an existing job, and grant Sam build, configure and read.

Step 1: Check, do not create

The task says to grant permissions to users. It does not say to create them, and both already existed.

Worth being deliberate about, because creating a second rohan leaves the grant attached to an account nobody logs into, and the symptom is a user who still cannot do anything. That reads like a permissions problem for as long as you let it.

Step 2: Install the plugin, again

Matrix Authorization Strategy is not part of Jenkins. Its own page describes it as offering "matrix-based security authorization strategies (global and per-project)", and on a stock Jenkins the Security page has no matrix option at all.

Same finding as Day 70, and it is worth repeating because the task's wording assumes it. A task that says "using a project-based matrix authorization strategy" is describing a plugin, and the authorisation half cannot start until it is installed.

Step 3: The step the task does not mention

Grant Overall/Read to authenticated users, globally, before touching the job.

Without it, logging in as Rohan gives an access-denied message naming the missing Overall/Read permission, so you cannot verify anything. The usual shorthand for this is that nothing works without Overall/Read, and that overstates it.

Jenkins is careful here: "While Overall/Read is a prerequisite for more extensive access to Jenkins, some features are available without it." The plugin says the same from the other side: "A user not granted read access to Jenkins in general will not be able to use most of the other permissions."

Most, not all. So the accurate version is that Overall/Read gates almost everything a person would recognise as using Jenkins, and granting it to authenticated users is a real grant rather than a formality. Every logged-in user can then see that the instance exists and enumerate what they have.

If you want to avoid that, Jenkins has a narrower tool. Job/Discover is documented as "only useful if anonymous users have Overall/Read permission, but not Job/Read", which is the shape for hiding specific jobs from users who can still reach Jenkins.

Step 4: The six permissions come from three places

This is the part I would have got wrong by reading the grid's column labels.

Jenkins groups its permissions as Overall, Agent, Job, Run, View and Credentials. The Job group is Build, Cancel, Configure, Create, Delete, Discover, Move, Read and Workspace. Four of the task's six names map straight onto it:

Task name Documented permission
build Job/Build
cancel Job/Cancel
configure Job/Configure
read Job/Read

The other two do not.

There is no Job/Update. The only documented Update permissions are Run/Update and Credentials/Update, and the one that appears in a per-job grid is Run/Update, which sits in the Run group and concerns individual builds rather than the job's configuration.

And there is no documented permission called Tag anywhere on Jenkins' permissions page. Tag comes from a plugin, and shows up in the grid only when that plugin is installed. I could not work out which plugin supplies it in this lab, so I am leaving that as unknown rather than filling it in.

Which makes the lesson a practical one rather than a pedantic one. A permission grid looks like one decision and is several, and a column header does not tell you whether you are granting something about the job or about its builds. Read the grid by column group, not by column label.

Step 5: Log in as both of them

The grid tells you what was ticked. It does not tell you what works.

Logging in as Sam and confirming the job can be configured and built, then as Rohan and confirming the same plus what he has extra, is the only check that distinguishes a configured grid from a working one. Day 70 made this point for a single read-only user, and it is stronger with two users who are supposed to differ.

What the docs recommend

Granting on the job rather than globally is the whole point, and the documentation explains why in one line. Global grants apply everywhere: "Permission applying to such items or agents that are granted in the global configuration apply to all of them." Per-item grants sit on top of that, additively, under the default mode: "Permissions explicitly granted on individual items or agents will only add to permissions defined globally."

So Rohan's six permissions granted globally would have given him them on every job, present and future. On the job, the scope stays where the task put it, and the global grid carries only the Overall/Read everyone needs.

There is one thing in this task worth more worry than the permission names, though, and it is that both users got Configure.

A freestyle job's configuration is its commands. Job/Configure lets a user change the build steps, which means anything in those build steps is visible to and editable by that user, including a password typed into a shell command. If the job runs on the controller rather than an agent, that command runs where Jenkins itself lives.

That is not a hypothetical in this series. Day 76's job puts a database password directly into its shell step. Anyone with Configure on it can read that password. Granting Configure is closer to granting shell access than the word sounds, and the fix is to keep secrets out of job configuration rather than to be careful about who gets Configure.

Task 2: Yesterday's VM, from the CLI

The task: the same self-configuring nginx VM as Day 22, named datacenter-vm, with the region named explicitly.

Everything from yesterday carried over. The Azure CLI base64-encodes --custom-data. --nsg-rule accepts only {NONE, RDP, SSH}, so port 80 needs az vm open-port. Cloud-init dispatches on the script's first line. And --storage-sku Standard_LRS is mandatory in this subscription, because the policy refuses whatever the undocumented default produces.

Step 1: Pass the region anyway

The resource group is already in eastus, so omitting --location would have worked. I passed it because the task names a region, which is the Day 4 rule: inheriting a default that happens to match is luck, not correctness.

eastus is the CLI name. "East US", as the task writes it, is the display name, and the CLI rejects it.

Step 2: The same commands took a different amount of time

Day 22:  attempt 1: 000    attempt 2: 000    attempt 3: 200
Day 23:  attempt 1: 200
Enter fullscreen mode Exit fullscreen mode

Same script, same image, same size, same region, one day apart. Yesterday nginx needed about 40 seconds after az vm create returned. Today it answered immediately.

Nothing changed on my side. The variance is in apt-get update and apt-get install, which depend on mirror latency and package cache state on the day.

That is the entire argument for polling a condition rather than sleeping an interval. A sleep 45 tuned to yesterday wastes 45 seconds today. A sleep 20 tuned to today fails yesterday.

The sharper version: a single immediate curl would have passed today and failed yesterday. That is the worst kind of check, because it works often enough to earn trust it has not demonstrated. A check that fails loudly is worth more than one that is usually right.

What the docs recommend

The task asked for two things that sound like one: HTTP reachable from the internet, and the nginx service running. Three checks, three different claims:

Check What it proves
curl returns 200 Reachable now, end to end: NSG rule, public IP, listening process, content
systemctl is-active Running now, independent of the network path
systemctl is-enabled Still running after a reboot

curl subsumes "running", since a stopped service cannot answer. What it cannot reach is is-enabled, which reports whether nginx starts at boot. That is invisible over HTTP, and it only becomes visible after the next restart, by which point nobody is looking.

systemctl enable --now in the boot script set both. Only the direct check confirms the enable half took effect, and az vm run-command invoke made that possible without SSH.

What I learned

Two lists that looked uniform and were not.

Rohan's six permissions read as one decision about one job. They are four permissions about the job, one about its builds, and one that Jenkins does not document at all. Nothing in the grid says so. You find out by reading the column groups instead of the column labels, or by granting something and discovering later it did not mean what you thought.

And two runs of the same deployment, with every input identical, differed by about 40 seconds in whether the thing I was checking for existed yet. Which is the more useful of the two findings, because it tells you something about verification rather than about Azure: if a check would have passed today and failed yesterday, it was never a check. It was a coin flip that happened to land well.

So here is the Day 73 question. Which of your verification steps would still pass if the thing it verifies had not finished yet?

Day 73 down. Twenty-seven to go.

Top comments (3)

Collapse
 
merbayerp profile image
Mustafa ERBAY •

Great work as always, Nnamdi! 👏

Two things really stood out to me in this one.

First, the Jenkins permissions. Job/Configure sounds harmless until you remember that modifying build steps can effectively mean executing arbitrary commands with the privileges of the Jenkins agent. That’s a much bigger responsibility than the permission name suggests.

Second, your Azure deployment timing observation. This is something we encounter in real production environments too. A deployment command returning successfully doesn’t necessarily mean the application is ready to serve traffic.

I’ve always preferred checking actual conditions over relying on fixed sleep intervals. Even better when those checks have sensible timeouts and report exactly what failed.

One small distinction worth keeping in mind: systemctl is-enabled confirms the service is configured to start at boot, but an actual reboot test is stronger evidence that it will successfully start.

I really like how you’re moving beyond simply completing tasks and questioning what your verification steps actually prove.

73 days down, 27 to go! Keep going, brother! 🚀

Collapse
 
ndcodes profile image
Nnamdi Felix Ibe •

Thank you, brother. These replies have become the part of the day I look forward to. 😄

Your reboot point is the right one, and it draws a line I hadn’t thought about. A reboot tests systemd and the package that’s already installed, and deliberately not the thing that installed it. cloud-init’s scripts-user module runs per instance, so the custom-data script doesn’t run again on a restart. That’s two claims hiding in one test: nginx will come back, and the provisioning would work again. A reboot only answers the first.

Which is the same lesson as the 40 seconds, one layer down. I’d have read a clean reboot as proof the whole boot path was sound.

Twenty-seven to go, and thank you for being here for them, my friend. ❤️

Collapse
 
merbayerp profile image
Mustafa ERBAY •

That genuinely means a lot, brother! ❤️ And believe me, following your journey has become something I look forward to as well. 😄

You’ve nailed the distinction. A successful reboot proves the service can recover from a restart, but it says nothing about whether the original provisioning process is reproducible.

That’s why I like treating provisioning, service readiness, and recovery as three separate verification targets. A fresh deployment tests the first, health checks test the second, and a reboot tests the third. Each can succeed while another fails.

And that’s what makes this series interesting to follow. You’re not just documenting successful commands anymore; you’re questioning the evidence behind every success.

I’ll be here for the remaining 27, my friend. Keep challenging those assumptions! 🚀❤️