Day 80 of DevOps, Day 30 of Azure. Both tasks today put a credential somewhere it did not belong, and in both cases the documented way out was one setting away.
A Jenkins job types a server password into a shell command, three times, in plain text. And an Azure SQL server ends up open to the internet with its admin password as the only thing in the way, behind a firewall rule whose notation means something other than it appears to.
One Jenkins task, one Azure task. Chain two jobs so a restart follows a deployment, then create a managed SQL database. The tasks come from the KodeKloud Engineer platform.
Task 1: Two jobs, chained
The task: nautilus-app-deployment pulls the latest code into /var/www/html, and manage-services restarts httpd afterwards.
Step 1: The chaining is the good part
Splitting this into two jobs is right, and worth saying before the rest of this section pulls the implementation apart.
Deploy and restart are separate concerns with separate failure modes. Chained, the restart does not run on a failed deployment, and manage-services can be triggered on its own when that is all you need.
Jenkins does not document the post-build action that does it. The "Build other projects" action, and the upstream and downstream relationship it creates, has no page under jenkins.io that I could find. Fourth day in this batch where the classic UI feature a task asks for is undocumented, after the Execute shell step, freestyle parameters, and the entire node configuration form.
Step 2: One plugin that does nothing
The prerequisites list three plugins, and one of them never gets used.
Publish Over SSH is described as "Send build artifacts over SSH". Everything it adds targets an SSH server defined under Manage Jenkins and Configure System, where each server needs "a name, hostname, username, and remote directory".
None of that was configured, and nothing in either job references it. The chaining uses the post-build action, and both jobs run their commands through Execute shell on the stapp01 agent.
Which is correct, and it is Day 75's work paying off: once the agent runs on the target server, a tool for reaching that server from somewhere else is redundant. The walkthrough's stated reason for installing it is an accurate description of the plugin. It is just not what this task needs.
Worth noticing as the mechanism by which plugin lists grow. Three installed, two used, and nothing ever says which is which.
Step 3: The password, three times
# nautilus-app-deployment
echo 'app-server-password' | sudo -S git config --global --add safe.directory /var/www/html
cd /var/www/html
echo 'app-server-password' | sudo -S git -C /var/www/html pull origin master
# manage-services
echo 'app-server-password' | sudo -S systemctl restart httpd
sudo -S is documented and does exactly what it is being used for: "Write the prompt to the standard error and read the password from the standard input instead of using the terminal device", with "The password must be followed by a newline character", which echo supplies. Sudo's own manual attaches no security warning to the flag, and I would rather say that than imply one exists.
The problem is not the flag. The build step lives in the job's config.xml, and anyone with Job/Configure on either job can read it in a browser. Day 73 granted Job/Configure on a job to two users, and nothing in the permission grid suggested that was a credential handover.
What the docs recommend
Here is what makes this worth more than "don't put passwords in jobs." The password is the fourth consequence of one unfixed thing, and you can watch the chain form.
/var/www/html is not writable by tony. So the job uses sudo. So the password goes into the build step. So git refuses to work in a repository owned by a different user than the one running it, because sudo guarantees that mismatch. So a safe.directory entry gets added to silence it.
Four consequences, one cause. Day 79's walkthrough solved the same underlying problem by checking the directory was owned by the job's user. Day 77's assumed it. This one escalates instead, and then spends three more lines managing the escalation.
One chown -R tony /var/www/html, by hand, once, and the jobs become:
git -C /var/www/html pull origin master
No sudo, no password, no safe.directory. (I could not retrieve git's own wording for that setting this time, so I am reading its purpose from the fact that the command is there at all: it would not be needed if the ownership matched.)
If the ownership genuinely cannot change, the second-best answer is a sudoers rule that needs no password, scoped to the two commands. Which is what Days 71 and 74 were quietly relying on without my noticing it was a design decision rather than a lab convenience. The third is a Jenkins credential binding, so the secret sits in the credential store behind its own permissions rather than in the job text.
Two smaller things in the same four lines. cd /var/www/html does nothing, because git -C already names the directory. And sudo -n would answer the question the password pipe is working around: it is documented to "Avoid prompting the user for input of any kind", and "If a password is required for the command to run, sudo will display an error message and exit." Run it once with -n, and you find out whether tony needed a password at all.
One last consequence of leaving it as written. git pull under sudo runs as root, so the files it writes into the document root become root-owned, and any later attempt to deploy without sudo fails. The escalation reinforces itself.
Task 2: A managed SQL database
The task: a Basic-tier database on a new logical server, 2 GiB, locally-redundant backups, publicly accessible.
Step 1: The server is not a server
An Azure SQL server is a logical container. It holds databases, the administrator login and the firewall rules, and it has no CPU, no memory, no patching, and no cost of its own. Billing is per database.
Different shape from AWS RDS on Day 31, where the instance is the billable compute and the thing you size:
| AWS RDS | Azure SQL | |
|---|---|---|
| Billable unit | The instance | Each database |
| Sizing applies to | The instance | Each database |
| Network control | Security group | Server-level firewall rules |
So two databases on one Azure SQL server are sized and billed independently. Two on one RDS instance share its compute.
Step 2: Three layers, not two
"publicNetworkAccess": "Enabled"
That only means the endpoint exists. By default, the server-level firewall "blocks all access to the public endpoint for the logical server", so the server would be reachable in name and unusable in fact.
I had written this up as two independent layers, the same shape as blob privacy on Day 16. There is a third, and Microsoft puts it in one sentence that is easy to read past: "Creating a firewall setting only allows clients to try to connect to your server. The client must still provide the necessary security credentials."
Endpoint enabled, source permitted, credentials accepted. The rule this task needed opens the middle one to the entire internet:
--start-ip-address 0.0.0.0 --end-ip-address 255.255.255.255
Which leaves the admin password as the only remaining control. Same position as the Jenkins job, arrived at from the other direction.
Step 3: The rule that means something else
| Rule | Meaning |
|---|---|
0.0.0.0 to 255.255.255.255
|
The entire internet |
0.0.0.0 to 0.0.0.0
|
Allow Azure services and resources to access this server |
The second is not a single-address rule. Microsoft: "To allow applications hosted inside Azure to connect to your SQL server, you need to enable Azure connections. To do this, create a firewall rule with starting and ending IP addresses set to 0.0.0.0." The portal toggle creates the same thing, named AllowAllWindowsAzureIps.
I had noted that it permits resources in any subscription. Microsoft's own warning is sharper, and the scope is wider than the toggle's label suggests: "Enabling this option allows connections from all Azure services, including services running in other customers' subscriptions. This rule doesn't restrict access to your subscription or resource group."
One detail I had missed, and it is the reassuring one: "By default, during creation of a new logical SQL server from the Azure portal, the Allow Azure Services and resources to access this server setting is set to No." Off unless asked for, which is the right default.
For anything real, Microsoft names the alternatives: "consider using virtual network service endpoints or private endpoints instead of the AllowAllWindowsAzureIps rule."
What the docs recommend
Basic is at its ceiling here in more than one dimension, and the one I had not recorded is the one that would matter.
5 DTUs maximum, 2 GB maximum storage. So asking for 2 GiB is asking for the largest database the tier allows, not a modest allocation.
And the backup window: Basic gets "1-7 day retention (default 7 days)", where Standard and Premium both offer "1-35 day retention". Long-term retention up to 10 years is available on all three.
Set that beside the backup redundancy this task asked for. Local is locally-redundant, three copies in one datacentre, and the only option that loses backups in a datacentre failure. So this database's recovery story is one week of backups, in one building. Both halves of that were requirements rather than choices, and together they are worth seeing as a whole.
One correction to my own note. I had written that vCore is "the current recommendation for anything real". Microsoft does not say that. The DTU model is described as "best suited for most customers with typical workloads", and vCore is presented as an alternative with features DTU lacks, such as Serverless and Hyperscale, plus control over hardware configuration. Preferring to reason about compute and storage separately is a fair argument. It is mine, not Microsoft's.
What I learned
Both tasks ended with a password as the last line of defence, and in both cases that was a consequence of something earlier rather than a decision anyone made.
The Jenkins one is the clearer lesson because you can follow the chain. The directory had the wrong owner. Everything after that, the sudo, the password in plain text, the git ownership workaround, exists to work around the first thing. Nobody decided to put a credential in a job. They decided not to run chown, and four lines later there is a password in a file two people can read.
The Azure one is a notation trap on top of a real exposure. 0.0.0.0 to 0.0.0.0 looks like the narrowest rule you could write and is one of the broadest, and the thing I had under-stated is how broad: all Azure services, in anyone's subscription. Meanwhile, the rule this task did ask for opens the server to the internet, and the Basic tier caps the backups that would get you out of trouble at seven days in one datacentre.
So here is the Day 80 question. The last credential you found hardcoded somewhere: what was it working around, and is that still true?
Day 80 down. Twenty to go.
Top comments (0)