[{"body":"","link":"https://blog.terakedis.dev/tags/architecture/","section":"tags","tags":null,"title":"Architecture"},{"body":"","link":"https://blog.terakedis.dev/categories/","section":"categories","tags":null,"title":"Categories"},{"body":"","link":"https://blog.terakedis.dev/tags/enterprise-integration/","section":"tags","tags":null,"title":"Enterprise-Integration"},{"body":"","link":"https://blog.terakedis.dev/tags/lessons-learned/","section":"tags","tags":null,"title":"Lessons-Learned"},{"body":"","link":"https://blog.terakedis.dev/post/","section":"post","tags":null,"title":"Posts"},{"body":"","link":"https://blog.terakedis.dev/","section":"","tags":null,"title":"Robert Terakedis"},{"body":"When we started modernizing the LMS infrastructure, the first thing someone asked was: \u0026quot;Can we just add SSO to the LMS?\u0026quot;\nIt's a reasonable question. It sounds like a small ask — flip a switch, wire up a login button, done. But the moment you start pulling on that thread, you find out it's not a switch at all. It's a decision about where identity lives across every system you own, and getting it wrong means you end up with the same mess you started with, just with a single sign-on button in front of it.\nThe landscape we inherited Before the redesign, we had multiple user databases that didn't know about each other. The LMS had its own user table. The lab had its own. Salesforce was the authoritative source of truth for customer and partner users, but nothing downstream actually trusted it. Each system maintained identity independently. In practice, that meant a lot of things broke silently. Someone changed companies in Salesforce, but their LMS profile didn't update because the LMS didn't know about the change.\nThe pain wasn't dramatic. It was death by a thousand small inconsistencies: mismatched email addresses, mismatched passwords, stale enrollments, audit headaches, and the ongoing cost of someone manually keeping everything synchronized when customers or partners ran into those issues.\nThe actual question The right question wasn't \u0026quot;can we just add SSO.\u0026quot; It was: who is the source of truth for user identity, and what should every other system trust?\nThat reframe matters because it forces you to think about the whole flow, not just the login screen. If you bolt SSO onto the LMS without answering that question, you might still have five systems maintaining their own user tables — they just share a login now. The sync problem doesn't go away. The complexity doesn't go away. It just moves somewhere less visible.\nIn our case, the answer was obvious: Salesforce. It was already where customer and partner information lived, it just wasn't being used across the board.\nThe architecture: Salesforce as the identity spine The design was straightforward in concept. Salesforce Experience Cloud became the identity provider. When a user logs into the LMS or the lab environment, they authenticate through Salesforce via OIDC. The downstream systems don't maintain their own user tables for auth — they trust what Salesforce says about who someone is.\nWe landed on OIDC instead of SAML for a practical reason: at the time, the LMS had a limit on how many SAML integrations it could support, and that slot was already consumed by another integration. OIDC was available, worked fine for our use case, and honestly wasn't a hard tradeoff once we understood the constraint.\nThe flow looks like this:\nNew external users are added to Salesforce (creating an Experience Cloud community user record) User navigates to the LMS and hits \u0026quot;Log in with Salesforce\u0026quot; LMS redirects to Salesforce OIDC, user authenticates Salesforce returns identity claims to the LMS LMS provisions or updates the user based on those claims Same flow applies to the lab environment When someone leaves the organization and their Salesforce record is deactivated, they lose access to everything on next login. No manual cleanup required.\nThe gotcha: community users aren't uniquely keyed Here's where the \u0026quot;just flip a switch\u0026quot; assumption really falls apart.\nEverything in Salesforce is an object - Community Users are objects, Contacts are objects, Licensed Users are objects, etc. In the case of our Salesforce \u0026quot;Identity Provider\u0026quot;, Community Users are derived from Contacts. Sounds easy right? Here's the tricky part - Contacts and Community Users change constantly: job changes, company acquisitions, email address updates. The problem I ran into was the lack of guardrails around modifications to Contact and Community User records after creation. The only consistent unique identifier across all the systems we wanted to federate (LMS, labs, knowledge base, etc.) was email address.\nThat's a common SSO assumption, and it's usually fine - until you discover that years of organic data entry left you with invalid addresses, duplicates, or mismatches between systems. The fix required manual review of the affected records. Not fun. But it also made visible a data quality problem that had existed quietly for years before SSO gave it consequences.\nLegacy user migration Before we could turn on SSO, we had to deal with the existing LMS user population — people who'd been in the system before Salesforce became the source of truth. The approach was two-pass. First, we disabled any LMS user account with a last login date more than two years old. Those users had almost certainly completed their required training and aged out of it — the two-year mark aligned with training \u0026amp; certification expiry cycles. If any of them needed access again, they could be re-provisioned through the new flow.\nFor everyone else, we matched existing LMS users to Salesforce community user records by email address. Where the match was clean, it was automated. Where it wasn't — the duplicate/mapping problem described above — it required manual resolution.\nWhat about when Salesforce goes down? The honest answer is: if Salesforce is down, new logins fail. That's the tradeoff you accept when you make one system the identity spine. We accepted it because Salesforce already had strong availability SLAs and the organization was deeply dependent on it for much more than just training access.\nThat said, there were break-glass accounts tied to a separate SSO — administrative accounts that could still get in if needed. And if absolutely necessary, an LMS admin could assign a password directly to a user's account. That's not a primary support path, but it exists for genuine emergencies. Active sessions survive an outage; only new logins are blocked.\nIs it a perfect fallback? No. But it was appropriate for the risk level and the operational reality.\nThe downstream impact Once the identity architecture was in place, a few things got dramatically simpler.\nAccess became automatic. Instead of someone manually adding users, they simply got access following the same familiar login flow they already used for other systems.\nOffboarding became a non-event. Deactivate in Salesforce (or move to a profile without access), access goes away. No checklist. No support ticket. No \u0026quot;we forgot to revoke their LMS access.\u0026quot;\nAuditing got easy. Every access was traceable back to a Salesforce identity. The audit trail was automatic, not assembled after the fact.\nWhat I'd do differently A few things I'd handle differently if I started over:\nNail down the unique key earlier. The community user duplicate problem cost us time we didn't expect to spend. Before any migration work starts, audit the identity data in both systems and agree on what \u0026quot;same user\u0026quot; means. Email alone isn't enough if either system has had years of organic data entry.\nUnderstand the guardrails (or lack of them) in place. I didn't fully understand how Contact and Community User records could affect each other after initial creation. Identifying the gaps earlier would have given us time to start cleanup before it became a blocker.\nClarify the outage story up front. We landed in a fine place, but the conversation about \u0026quot;what happens if Salesforce goes down\u0026quot; should happen before architecture review, not after. Stakeholders need to consciously accept that tradeoff.\nThe actual lesson SSO looks like a flip switch from the outside. From the inside, it's a question about where truth lives across every system you're connecting — and whether the data in those systems is actually trustworthy enough to build on.\nOnce we answered that question — Salesforce is the source of truth, everything else trusts it — the architecture followed naturally. OIDC handled the protocol. Just-in-time provisioning handled new users. Deprovisioning became a side effect of Salesforce offboarding. The complexity didn't disappear; it just moved into a place where it was visible and manageable.\nThat's the difference between bolting on SSO and actually solving the identity problem.\nComments welcome!\n","link":"https://blog.terakedis.dev/post/sso-is-not-a-flip-switch/","section":"post","tags":["enterprise-integration","lessons-learned","architecture"],"title":"SSO Is Not a Flip Switch"},{"body":"","link":"https://blog.terakedis.dev/tags/","section":"tags","tags":null,"title":"Tags"},{"body":"","link":"https://blog.terakedis.dev/categories/technology/","section":"categories","tags":null,"title":"Technology"},{"body":"","link":"https://blog.terakedis.dev/tags/automation/","section":"tags","tags":null,"title":"Automation"},{"body":"","link":"https://blog.terakedis.dev/tags/azure/","section":"tags","tags":null,"title":"Azure"},{"body":"","link":"https://blog.terakedis.dev/tags/compliance/","section":"tags","tags":null,"title":"Compliance"},{"body":"","link":"https://blog.terakedis.dev/tags/privacy/","section":"tags","tags":null,"title":"Privacy"},{"body":"Last year I got pulled into a project where the team was drowning in spreadsheets. Not the good kind, but rather the unfortunate kind: manual user provisioning lists, copy-paste operations between systems, and a recurring cloud bill that kept climbing because of over-provisioned resources sitting idle.\nThe kicker? Along with all the manual inefficiences, we incurred $300,000/year in overprovisioned cloud spend. And that was before we even talked about the staff time or the risk of things breaking when a step in the process was missed.\nLet me walk you through how I turned this liability into a strength, and what I learned about why manual processes are more expensive than they look.\nThe Problem: When Spreadsheets Become Your System It started with a learning management system (LMS) that handled course enrollments and training materials. Users needed access to hands-on labs for practical exercises, and video conferencing for live sessions. On paper, it sounded straightforward: add a user to a course, provision their lab environment, and set up their meeting access.\nIn reality, the process had grown organically without any integration between tools. Learners sign-up for a course in the videoconferencing tool. Export a CSV from the video conferencing tool, import it into the LMS. Manually cross-reference the list with an Excel spreadsheet tracking lab tenant assignments. If a user changed sessions or modalities (instructor-led vs self-paced), all three systems had to be updated manually: LMS, lab infrastructure, and video conferencing. The cloud spend was the most visible symptom. The team was bulk-provisioning lab tenants for entire courses, even though most sat idle for weeks.\nMapping the Chaos: Four Systems, Zero Integration To fix this, I first had to understand the full scope. We had four main systems:\nCRM system (acting as the source of truth for user identities) LMS (for course management and content delivery) Lab infrastructure (cloud-based environments for hands-on practice) Video conferencing platform (for live sessions and recordings) Each system was an island. There were no APIs connecting them, no automated workflows. Everything relied on manual exports, imports, and human coordination. Any time a step was missed, users could end up with mismatched access status between the LMS, Video Conferencing tool, and hands-on labs.\nThe Business Case: Making Invisible Work Visible Before we could sell automation, we needed to quantify the problem. I spent time shadowing the team and tracking their workflows. Here's what we found:\nTime spent: 15-25 hours per month on cloud provisioning and sync tasks Cloud waste: $300K annually on idle lab tenants Risk exposure: High potential for business interruption if a learner signed up late and was missed during the spreadsheet loads. The $300K figure wasn't just a number, it was the annual cost of over-provisioning lab environments. By moving to just-in-time provisioning, we could eliminate that waste entirely.\nThe Solution: Automation and Integration I decided we needed to treat the CRM as our single source of truth for user identities and roles. From there, I built automated bridges between all the systems.\nFirst, I implemented SSO through the CRM's identity provider to connect everything. This was an easy transition for learners, who were already familiar with this login for our Knowledgebase. A user with access to the Knowledgebase (customer or prospect), now had access to the LMS. This also eliminated manual user provisioning in the LMS.\nThe Hard Part: Cleaning Up the Legacy User Mess Of course, we couldn't just flip a switch. The systems were already in production with users, and we had years of accumulated data drift - users in some systems but not others, stale permissions, mismatched roles. This kind of drift is inevitable when systems don't talk to each other. It's not a failure of diligence, it's a failure of architecture. I worked with the LMS admin to find users with last-login dates older than a certain threshold, and we disabled those accounts. Additionally, the LMS admin drafted communications instructing learners on how to use the new login flow. Using Just-in-Time provisioning through the SSO provider, we were able to dynamically map existing user accounts to be SSO-enabeld, and any new learner accounts created dynamically as they authenticated.\nAutomating the Video Conferences: Work from a Single Tool As mentioned earlier, there was also a video conferencing tool in the mix. Under the previous setup, the Training team had to set up Meetings for every instructor-led training class, which was then used as the \u0026quot;sign-up\u0026quot; for the class. We changed this around by moving the \u0026quot;sign-ups\u0026quot; from the conferencing tool, to the LMS. I got the video conferencing tool integrated into the LMS natively, and worked with the Training Team to start building their Instructor-Led course catalog out in the LMS. Now, each instructor-led course is populated with \u0026quot;Sessions\u0026quot; for each instance of the training, and during the setup the trainer opts for the LMS to use the conferencing tool.\nThe net result is that as learners sign-up for instructor-led courses, the LMS automatically creates and adds the learner to a conference specific to that session. In the trainer's view of the LMS, they see the \u0026quot;host\u0026quot; join link, while the learners see the \u0026quot;attendee\u0026quot; join link. Trainers only need to define courses and sessions in the LMS. Learners only need to find and sign-up for sessions in the LMS. One place to go for everyone.\nAutomating the Lab Provisioning: Webhooks and Workflows Working with our in-house workflow automation team and my coworker for lab support, we worked through a number of problems:\nHow to know a course is \u0026quot;lab-enabled\u0026quot;? How to know when a lab-enabled course starts/ends? How to know what type of course it is (self-paced versus instructor-led) \u0026lt; Webhooks just entered the chat \u0026gt;\nThe LMS provided a number of webhooks which allowed us to start with as subset of information, and then dynamically query the LMS and HOL systems to fill in the blanks. Using this we were able to build a number of workflows to act upon the Event happening in the LMS, and take appropriate action in the HOL system.\nThe Results: From Liability to Asset The transformation was dramatic:\nProvisioning time: From 1-2 days of manual work per course to under an hour of automated processing Cloud costs: Eliminated the $300K in idle tenant spend by moving to on-demand provisioning Staff time: Freed up dozens of hours per month for the training team Simplicity: Trainers and Learners start from the LMS, leading to a repeatable experience. The biggest win wasn't the speed or the cost savings—it was the peace of mind. Provisioning became something we could rely on, not a source of constant worry.\nWhat I Learned This project taught me a few hard truths about enterprise systems:\nManual processes feel cheap because their costs are invisible. They're baked into staff time and occasional crises, not line items on a budget. But when you add it all up (time, waste, risk), the real cost is staggering.\nAutomation isn't about doing the same thing faster. It's about making work auditable, repeatable, and scalable. And sometimes, it's about eliminating work entirely.\nThe most valuable systems are the ones that let you do more with less. Provisioning is foundational. If you get it right, everything downstream gets easier. If you get it wrong, it becomes a bottleneck that limits your entire operation.\nFinally, never underestimate the power of cleaning up your data before automating. You can't automate chaos; you have to fix the foundation first.\nComments welcome!\n","link":"https://blog.terakedis.dev/post/the-300k-spreadsheet/","section":"post","tags":["automation","azure","enterprise-integration","lessons-learned","privacy","compliance"],"title":"The $300K Spreadsheet: How Manual Provisioning Became a Liability"},{"body":"","link":"https://blog.terakedis.dev/tags/backblaze/","section":"tags","tags":null,"title":"Backblaze"},{"body":"","link":"https://blog.terakedis.dev/tags/cloudflare/","section":"tags","tags":null,"title":"Cloudflare"},{"body":"","link":"https://blog.terakedis.dev/tags/devops/","section":"tags","tags":null,"title":"Devops"},{"body":"When I migrated this blog to Hugo, I had to figure out where images would live. My original thought was simple enough: store them in the GitHub repo. That worked fine for six months. Then I started thinking about what happens when I have hundreds of posts with images, and the repo turns into a photo dump.\nI looked at Cloudflare Images next. It seemed obvious—I'm already paying for Cloudflare for DNS and SSL. But the pricing pushed me away: the transform API costs add up if you're generating variants (WebP, different sizes), and I'd be locking myself into Cloudflare's image manipulation if I ever wanted to migrate. That didn't feel like room to grow.\nSo I landed on Backblaze B2 + Cloudflare. Eight months in, I'm still surprised at how cheap and invisible this setup is. Here's why it works, and how to replicate it.\nWhy not Cloudflare Images? Cloudflare Images charges per image stored (it's not terrible—$5/month for 100,000 images) plus per transformation. If you're resizing or format-converting on the fly, that cost compounds. And you're locked in: if you ever want to move images somewhere else, you'd have to rewrite all your URLs.\nB2 + Cloudflare separation means I own the origin. If I wake up one day and want to migrate somewhere else, I just change a DNS CNAME. No URL rewrites, no broken links.\nWhy not just GitHub? GitHub is great for source control, but it's not a CDN. Every image request comes from the GitHub servers—no caching, no edge distribution, same bandwidth hit whether someone's in Tokyo or Toronto.\nAlso, GitHub discourages using your repo as a generic file host. A few hundred images is fine; thousands and you're fighting against the platform's assumptions. B2 is designed for exactly this.\nThe B2 + Cloudflare approach The setup is dead simple: store images in a public B2 bucket, point a Cloudflare CNAME to B2's endpoint, add some transform rules to rewrite paths, tweak the response headers for caching. That's it.\nHere's the flow:\nBrowser requests https://files.terakedis.dev/images/photo.jpg Cloudflare intercepts → applies a transform rule to add the B2 bucket prefix → requests /file/euc-rt-files/images/photo.jpg from B2 B2 returns the image + response headers Cloudflare strips B2 metadata headers, sets cache to 24 hours Image gets cached globally on Cloudflare's edge → user gets it from nearby No Workers, no Lambda functions, no complicated auth. Just DNS + rules.\nThe free tier + free egress math Here's what makes this actually free for small blogs:\nBackblaze B2:\n10 GB of egress per month, free 1 GB of storage, free $0.006 per GB after that (or about $6/month per TB) Cloudflare:\nFree tier includes DNS, SSL, and unlimited transform rules No per-request fees, no overage charges You can have as many rules as you want The kicker: Backblaze has a partnership with Cloudflare. When traffic comes through Cloudflare, B2 doesn't charge egress. At all. This is huge and I see a lot of people miss it.\nRight now my blog has 13 images stored on B2 (14.1 MB total). Even if I had 1,000 images, I'm still comfortably in the free tier. The partnership means every byte served through Cloudflare's cache costs me exactly $0.\nYou only start paying when:\nYour B2 storage exceeds 1 GB (unlikely unless you're hosting video) You hit egress from B2 that doesn't go through Cloudflare (direct B2 links, for example) For a personal blog or small creator, this runway is essentially infinite.\nConfiguration to enable free egress: It's automatic—no special setup required. You just need:\nA public B2 bucket (readable by anyone) Cloudflare fronting it via CNAME B2's systems detect Cloudflare's IP ranges and waive egress That's it. The partnership handles the rest.\nHow the Cloudflare setup works Your Cloudflare rules do two things:\nURL Rewrite Rule:\n1Match: requests to files.terakedis.dev that don\u0026#39;t already have /file/ in the path 2Action: rewrite /images/photo.jpg → /file/euc-rt-files/images/photo.jpg This tells B2 which bucket to serve from. B2's direct API endpoint (f000.backblazeb2.com) expects paths in the format /file/\u0026lt;bucket-name\u0026gt;/...\nResponse Header Rules:\n1Rule 1: Strip B2 metadata (ETag, x-bz-* headers) 2Rule 2: Extend cache from 4 hours to 24 hours 3Rule 3: (optional) Add CORS headers if you need cross-domain image embedding The DNS CNAME does the rest: files.terakedis.dev → f000.backblazeb2.com\nWhat I got right It's cheap. I'm not paying anything, and even if I scaled to 10,000 images, I'd probably still be in the free tier.\nIt's durable. B2 has the same durability guarantees as AWS S3 (99.999999% — that's 11 nines). My images won't mysteriously vanish.\nIt's room to grow. If I ever need more bandwidth or storage, I don't rearchitect. I just start paying B2's usage fees. No migration, no URL changes.\nIt's decoupled. My image hosting isn't tied to my blog platform or my CDN. I can swap any piece without affecting the others.\nGotchas and the growth path Nothing has surprised me negatively. The only gotchas are things you should know upfront:\nB2's free egress only applies through Cloudflare. If you link directly to B2 (like from a social media post), that traffic is charged. So don't do that. Always link through your Cloudflare domain.\nB2 versioning. By default, B2 keeps all old versions of files. This is great for recovery but eats storage. You can disable it if you want, but I've left it on for peace of mind.\nCache invalidation. Since Cloudflare caches for 24 hours, if you replace an image, it takes a day to propagate. For a blog, that's fine. If you need instant updates, you'd need to purge the cache manually or shorten the TTL.\nAs for the growth path: I can scale this to hundreds of thousands of images before hitting any real costs. At some point, if traffic gets huge, paying for B2 storage and bandwidth is still cheaper than Cloudflare Images or other managed solutions.\nWho this is for This setup is for anyone hosting a static site (Hugo, Jekyll, Next.js, whatever) and wants cheap, durable image storage that's easy to move later.\nIt's especially good if you're a solo creator or small team and don't want to manage complex infrastructure. You get CDN speeds and global distribution for free.\nYou probably shouldn't use this if you need image transformation on-the-fly (dynamic crops, format conversion for different devices). Cloudflare Images handles that. Or if you need upload APIs and user-generated content—that's a different problem.\nBut for a blog where you control the images and you're happy uploading them by hand? This is unbeatable.\nThe move Setting this up took me about 20 minutes. The Cloudflare rules are the only slightly fiddly part, but the defaults work fine and I adjusted them for caching as I went.\nI've been running this for eight months now with zero issues. No surprise bills, no performance problems, no weird edge cases. Just images that load fast and cost nothing.\nComments welcome!\n","link":"https://blog.terakedis.dev/post/b2-cloudflare-image-hosting/","section":"post","tags":["cloudflare","backblaze","devops","hugo","architecture"],"title":"How to host blog images for free with Backblaze B2 and Cloudflare"},{"body":"","link":"https://blog.terakedis.dev/tags/hugo/","section":"tags","tags":null,"title":"Hugo"},{"body":"","link":"https://blog.terakedis.dev/tags/career/","section":"tags","tags":null,"title":"Career"},{"body":"","link":"https://blog.terakedis.dev/tags/life/","section":"tags","tags":null,"title":"Life"},{"body":"If you’ve ever struggled to keep your family’s schedule in sync—or just wanted a little less chaos in your mornings—the Calendar Max might be your new best friend. After seeing it pop up in a Mother’s Day promo, we decided to give it a shot. Spoiler: our living room wall makes me smile every time I walk by, and even our kids are excited to check what’s coming up next!\nOur Previous Calendar Solution Much like most families with both parents in full-time employment, my wife and I kept our calendars (mostly) sync'ed by emailing invites to each other. Every time there was a new event, work trip, kid activity, school activity, etc., we were emailing 3 different calendars. This mostly worked, but we ran into periodic issues:\nAn email address wasn't added, leading to an accidental calendar mis-match between work and personal calendars for one of us An update to a calendar appointment was needed, but the parent trying to make the modification wasn't the owner on the invite Adding to this, my wife wanted a better way to plan the week as a family. The small screen on an iPad or iPhone just wasn't enough to see what all was going on at a glance, nor was it easy to see who was doing what.\nEnter the Calendar Max If you haven't seen one of the advertisments, the Calendar Max from Skylight is a large, wall-mounted digital calendar designed to help families stay organized. It syncs with popular online calendars like Google and Outlook, displaying all your events, appointments, and reminders in a clear, color-coded format. With its touchscreen interface and easy-to-read display, the Calendar Max makes it simple for everyone in the household to see what’s coming up and keep schedules in sync.\nYes, the marketing sold me on it. But, when it got down to implementing things, I ran into a few issues:\nIf I synced both my wife and my personal calendars, we ended up with duplicates of all the invites due to our previous method of cross-invitations for all events Even syncing just one calendar meant I had to manually color-code all the events to different \u0026quot;profiles\u0026quot; on the Calendar Max The Skylight app does allow you to manage your calendar, but when you create events you had to pick BOTH the profile and the synced calendar. This seemed like a possible step to introduce issues since you could accidentally choose the wrong backing calendar for a profile entry. Setting up Multiple Calendars and Profiles on the Skylight App I wanted to make it super-easy for our family to color code all the envents on our calendar to know who was where. To do this, Skylight implements what they call Skylight Calendar Profiles. If you read through the help documents, you learn that profiles are not the same as calendars -- they are two distinct \u0026quot;objects\u0026quot; you manage in the Skylight App. To save you all the research, this is what you need to understand:\nProfiles relate to the members of your household (or any other types of event you want to track, like the trash days, etc)) Profiles can have different or similar colors, useful if you manage events for one person on two separate calendars (like work and personal, or chores and sports) Calendars are synced as separate objects to your Skylight account, and then mapped to a profile Calendars can be from popular services like Google, Outlook, etc, or public Internet calendars With that knowledge in mind, let's look at the process I went through to get things set up.\nCreating and Sharing our Google Calendars The first step was to share our Google Calendars. At the time, our kids did not have any Google accounts, nor did they have calendars anywhere (not even Apple calendars). We also needed permissions for my wife and I to see our kids' calendars. Here's what I did to get us set up and ready:\nRegistered both kids for a Google Account at Google Family Link, saving their username/password/passkey in our family password manager. Logged in to each kid's Google account to view their Calendar. In Google Calendar, when you mouse over the calendar name on the left side, there is a \u0026quot;3-dots\u0026quot; icon that you click to see a context menu. Click Settings and Sharing Under the Shared With heading, I added the personal (Google) email address for my wife and I with the Make Changes to Events permission and clicked Send My wife and I also did the same steps with our own personal Google calendars, granting each other \u0026quot;Make Changes\u0026quot; permissions. On my own calendar, I also created a secondary calendar for \u0026quot;Family\u0026quot; events (which I shared to my wife) and also have a few subscribed public calendars (such as U.S. Holidays, Sports Teams, and our City Trash Pick-Up Schedule) For my own calendar (the primary account I would use to sync the calendars to Skylight), I had to ensure all the calendars I wanted to view on Skylight were selected in Sync Settings Fixing our Calendar Invites This process took a few hours. We had to undo overlapping calendar invitations and assign each event to the correct calendar. I found this process easiest to do using the Google Calendar app on my iPad (though my wife did hers on her phone). Starting with the current week, and working our way into the future, we would open each calendar invite and modify the event to remove duplicate personal calendar invitations. Because we both work at companies with restrictive email policies, we can't directly sync our google calendars to our work calendars so we're stuck having to include work emails here. Conversely, we also went into our work calendars and removed the multiple personal email invitations for things like Work Trips, all-day On-sites, etc. Now we simply create the event on our work calendar and then email our own personal email address ONLY.\nHowever, as we unraveled the personal calendar entanglements, we were also able to move the \u0026quot;owners\u0026quot; of those invites to each respective calendar. In other words, rather than my calendar owning the invitation for my son's soccer practice, his calendar owned it. This is an important step that plays into the Profile setup later...\nSyncing the Google Calendar to Skylight With all the calendar invitations fixed, I went into the Skylight app on my phone and clicked on Synced Calendars. This was pretty self-explanatory, but I opted to sync in a Google Calendar (with 2-way integration) and logged in when prompted. The interesting part here is that I've only synced Skylight with MY Google calendar, but since I can view and/or modify all the related calendars we need to see, there was no need to add additional calendars.\nConfiguring the Profiles in Skylight Within the Skylight app, I clicked on the Profiles button and started adding Profiles for everyone in our family. I also added a profile for \u0026quot;Family\u0026quot; events and a profile for all the other calendars I planned to sync to the Calendar Max (Sports Teams, Garbage Day, etc). The trick here is that as you create and name the profile, you select a unique color AND the appropriate linked calendar from Google. For example:\nProfile Color Linked Calendar Me Green personalemail@gmail.com Wife Purple wifesemail@gmail.com Kid1 Blue kid1@gmail.com Kid2 Pink kid2@gmail.com Trash Grey Trash Calendar NFL Team Orange NFL Team Calendar Hopefully that makes sense! If not, feel free to ask questions below.\nThe Net Result Going Forward With this configuration completed, the Calendar Max is now showing all of our calendar events color-coded by family member. When we're away from the house, we can easily see a similar view using the Google Calendar app. We're also using the Google Calendar app to manage new events on a day-to-day basis. You may be asking \u0026quot;Why not just use the Google Calendar app?\u0026quot; Fair question, but as mentioned above we wanted a nice large-format view into the next few days so we could quickly plan in the morning. Additionally, it helps keep our kids in the loop as to what activities they have each day. The Calendar Max has quickly become our planning hub.\nFinal Thoughts Was it expensive? In my opinion, yes. That said, we started with a 15 inch calendar that we bought from Costco. By purchasing it from Costco, I figured we could easily return it if we didn't like the experience. On top of that, the Costco purchase also included a free 1-year Calendar Plus subscription, which we've been using to experiment with the Routines and Chores functionality. I've also found that the easiest way to share photos to the Calendar (for photo screensaving) is to select photos in the photos app and then share them to the Skylight App (versus selecting photos in the Skylight App directly).\nI'm definiely curious to hear your thoughts if you've been looking into the Skylight calendars. Leave a message below and hopefully the process outlined above will help get you to automatically synced/color-coded calendars.\n","link":"https://blog.terakedis.dev/post/setting-up-skylight-calendar-max/","section":"post","tags":["Career","Life"],"title":"Setting up our Calendar Max"},{"body":"","link":"https://blog.terakedis.dev/tags/docebo/","section":"tags","tags":null,"title":"Docebo"},{"body":"","link":"https://blog.terakedis.dev/tags/json/","section":"tags","tags":null,"title":"JSON"},{"body":"","link":"https://blog.terakedis.dev/tags/postman/","section":"tags","tags":null,"title":"Postman"},{"body":"I've recently been involved with a project at work to modernize the processes around Docebo, our Learning Management System (LMS). A significant portion of this project involves workflow automation and passing data/events between multiple systems. We accomplish this automation using webhooks and a number of API calls (which you can view at Docebo's API Browser). Early in the process, I stumbled onto the Docebo Community post about Using Postman (and the on-demand Community Coaching Session) which contained a downloadable Postman API collection. This was a GREAT starting point, but the collection used an interactive login to get oAuth2 tokens. I wanted to automate the authentication process and avoid the interactive logins when I was testing against our sandbox LMS instance.\nUnderstanding Docebo oAuth API Authentication Within the Postman collection, the original author configured the collection to use the Authorization Code + Implicit grant type authentication. In this scenario, Postman requires an interactive login to get your consent to generate the auhentication token. However, Docebo's API Authentication Documentation describes multiple oAuth2 API authentication methods, one of which is Resource Owner Password Credentials. In this authentication scenario, Postman makes a call to Docebo's oAuth Token URL and provides a number of formdata parameters in order to obtain an oAuth2 token. By using your vaulted Username and Password, Postman authenticates as you, allowing you to avoid the interactive login. Notice I mentioned vaulted credentials.\nSetting up the Postman Vault The Postman Vault was a relatively new feature to me. Basically, it's an encrypted collection of key-value pairs that remains local to your machine and doesn't get synced. This allowed me to keep my credentials local and secured while I reused them for my token authentication. Here's how to set it up:\nTo start, you need to access the Postman Vault by clicking the vault icon in the bottom right of the postman window. } The Postman Vault Button Next, you'll need to add the client ID, client secret, username, and password to the vault. } Adding Credentials to Postman Vault After that, you need to allow vault access from your scripts. From within your Postman vault, click Settings then enable the toggle for the option Enable support in Scripts. Click Enable in the pop-up and then close your settings screen.\nThe first time you call an API in the collection and your pre-request script runs, you'll have to Grant Access to the vault for your Collection. } Granting Collection access to the Postman Vault Configuring the Collection and APIs At the time I started creating this script (not this blog post), I had to make some modifications to the original collection I had downloaded. I mostly implemented these changes as I went along with my testing - no need to do it all up front.\nOn the Collection's Variables tab, I had to add a few new variables: access_token and expires_in. On the Collection's Authorization tab I had to change the Authorization type to Bearer Token and then set up the token to use the {{access_token}} collection variable my script modified. For each folder and API in the collection, I had to ensure the Authorization tab showed the Auth Type as Inherit auth from parent. Postman Pre-Request Script Now it was time to add the pre-request script. Here's how you do that:\nWith the Collection selected, click on the Scripts tab. Click on Pre-Request Script and paste the content of the script below. Save the Collection. 1var baseUrl = pm.collectionVariables.get(\u0026#34;base_Url\u0026#34;); 2var client_id = await pm.vault.get(\u0026#34;clientId\u0026#34;); 3var client_secret = await pm.vault.get(\u0026#34;clientSecret\u0026#34;); 4var docebo_uid = await pm.vault.get(\u0026#34;docebo_user\u0026#34;); 5var docebo_upw = await pm.vault.get(\u0026#34;docebo_user_password\u0026#34;); 6 7const tokenUrl = baseUrl + \u0026#39;/oauth2/token\u0026#39;; 8 9const getTokenRequest = { 10 method: \u0026#39;POST\u0026#39;, 11 url: tokenUrl, 12 body: { 13 mode: \u0026#39;formdata\u0026#39;, 14 formdata: [ 15 {key: \u0026#39;client_id\u0026#39;, value: client_id,}, 16 {key: \u0026#39;client_secret\u0026#39;, value: client_secret,}, 17 {key: \u0026#39;grant_type\u0026#39;, value: \u0026#39;password\u0026#39;,}, 18 {key: \u0026#39;scope\u0026#39;, value: \u0026#39;api\u0026#39;,}, 19 {key: \u0026#39;username\u0026#39;, value: docebo_uid,}, 20 {key: \u0026#39;password\u0026#39;, value: docebo_upw,} 21 ] 22 } 23}; 24 25pm.sendRequest(getTokenRequest, (err, response) =\u0026gt; { 26 const jsonResponse = response.json(); 27 const newAccessToken = jsonResponse.access_token; 28 const expires_in = jsonResponse.expires_in; 29 const newScope = jsonResponse.scope; 30 pm.collectionVariables.set(\u0026#34;access_token\u0026#34;, newAccessToken); 31 pm.collectionVariables.set(\u0026#34;expires_in\u0026#34;, expires_in); 32 console.log(\u0026#39;scope\u0026#39;, newScope); 33}); Understanding the Script To aid your understanding, let's walk through this script line-by-line:\nScript Line Explanation var baseUrl = pm.collectionVariables.get(\u0026quot;base_Url\u0026quot;); This gets the collection variable base_Url, which contains the URL of our docebo instance (example: https://sandbox.docebosaas.com). var client_id = await pm.vault.get(\u0026quot;clientId\u0026quot;); This gets the clientId value you've stored in the Postman Vault. This value is originally generated in the Add oAuth2 App screen in Docebo. var client_secret = await pm.vault.get(\u0026quot;clientSecret\u0026quot;); This gets the clientSecret value you've stored in the Postman Vault. This value is originally generated in the Add oAuth2 App screen in Docebo. var docebo_uid = await pm.vault.get(\u0026quot;docebo_user\u0026quot;); This gets the docebo_user value you've stored in the Postman Vault. This value is your actual username (or a username you specifically configure for API access) in Docebo. var docebo_upw = await pm.vault.get(\u0026quot;docebo_user_password\u0026quot;); This gets the docebo_user_password value you've stored in the Postman Vault. This value is the password associated to the account specified for docebo_user in the vault. const tokenUrl = baseUrl + '/oauth2/token'; This adds the oAuth2 Token URL path onto the base URL to create the token endpoint. Since we already store the base_Url in the collection to re-use in the URL field across all the API path definitions, we can re-use it in the pre-request script in the same way. The entire const getTokenRequest = { ... } block This sets up the form data that will be passed in the request to the token endpoint. The key items are as follows:\n* method: 'POST' sets the HTTP method Verb to POST\n* url: tokenUrl tells Postman where to send the request (using the tokenUrl derived from the base_Url in the Collection variables\n* body: tells Postman to add the following data to the body of the request\n* mode: 'formdata' tells Postman that the data in the body should be formatted like data gathered from an HTML form\n* {key: 'xxxxx', value: \u0026lt;variable\u0026gt;,}, are the lines where we include the values we need to POST in the form in order to authenticate for the token -- note that grant_type and scope are hardcoded as they are based on the authentication method we're using. pm.sendRequest(getTokenRequest, (err, response) =\u0026gt; { This is the line that tells Postman to send the request as we've configured it in the getTokenRequest block. const jsonResponse = response.json(); This saves the HTTP response as JSON-formatted data. const newAccessToken = jsonResponse.access_token; This saves the access_token out of the JSON data. const newScope = jsonResponse.scope; This saves the token scope out of the JSON data (mostly for reference/troubleshooting). pm.collectionVariables.set(\u0026quot;access_token\u0026quot;, newAccessToken); This sets the value of a collection variable that is used in the Authorization tab for collection as a whole (i.e. the top level of the collection - all subfolders and API's Inherit from this parent). pm.collectionVariables.set(\u0026quot;expires_in\u0026quot;, expires_in); This sets the value of a collection variable that currently isn't used. I was storing this for a potential future improvement (see below) console.log('scope', newScope); This logs the value for token scope to the Postman Console for troubleshooting purposes. Putting It Together With the Pre-request script in place, and the Collection and APIs modified, the authentication was working great. Here's a high level overview of what is happening:\nWhen I click Send on an API call, Postman first runs the Pre-Request Script. The Pre-Request Script pulls the secrets from the vault and the collection variables, sends the request to the oAuth2 token endpoint, and gets a token which it saves as the access_token variable for the collection. The API inherits the Authentication configuration from the Folder and Collection (parent), which sets the Auth Type to Bearer and uses the {{access_token}} from the collection variables. Postman sends the API request. Potential Improvements I created this script to speed up my workflow and was mostly concerned with just getting authenticated. That said, there's definitely room for improvement on this script. Were I to later iterate on this script, here's a few items I would modify:\nSaving the access_token to the Vault: I agree, saving the access_token to the collection isn't the most secure way to handle this, but I ran into some errors trying to set the access_token value into the vault during the sendRequest function. This is definitely something I need to research and fix. Integration with a Password Manager: Rather than using Postman's vault, I would prefer to leverage my current password manager. I don't particularly like that my username and password exist outside the vault, but since it's a sandbox environment I'm less concerned. I know the Enterprise licenses include integration with a few password managers, but I'm using the free plan. My thought is perhaps I can leverage the password manager's CLI utility as part of the pre-request script. To be continued... Token Re-Use and Expiry: In it's current form, this pre-request script requests a new oAuth2 token every time I execute an API call. To improve on this, I should be tracking the token expiry as a Collection variable. At the beginning of the script, I should be checking if the token is expired, and if not, re-use the current token rather than requesting a new one. JWT Assertions on the fly: I did experiment with JWT, but I wasn't able to figure out a method to create the JWT assertion on the fly. This one will require more digging and practice. Bonus Round - JSON Web Token Pre-Request Script I was also testing with JSON Web Tokens, but this is far more difficult to set up. If you go the JWT route, be mindful of how you create the assertion and what tools you're using to do so. While I used JWT.io to create my assertion, this was low risk as I was testing in a sandbox environment for a short period of time. If you are a brave soul that wants to try setting this up, here is a pre-request script that might help get you started:\n1var baseUrl = pm.collectionVariables.get(\u0026#34;base_Url\u0026#34;); 2var client_id = await pm.vault.get(\u0026#34;clientId\u0026#34;); 3var client_secret = await pm.vault.get(\u0026#34;clientSecret\u0026#34;); 4 5// Assertion from JWT.io using the Public/Private Keys 6var jwt_assertion = await pm.vault.get(\u0026#34;assertion\u0026#34;); 7 8const tokenUrl = baseUrl + \u0026#39;/oauth2/token\u0026#39;; 9 10const getTokenREquest = { 11 method: \u0026#39;POST\u0026#39;, 12 url: tokenUrl, 13 body: { 14 mode: \u0026#39;formdata\u0026#39;, 15 formdata: [ 16 {key: \u0026#39;client_id\u0026#39;, value: client_id}, 17 {key: \u0026#39;client_secret\u0026#39;, value: client_secret,}, 18 {key: \u0026#39;grant_type\u0026#39;, value: \u0026#39;urn:ietf:params:oauth:grant-type:jwt-bearer\u0026#39;,}, 19 {key: \u0026#39;assertion\u0026#39;, value: jwt_assertion,} 20 ] 21 } 22}; 23 24pm.sendRequest(getTokenREquest, (err, response) =\u0026gt; { 25 const jsonResponse = response.json(); 26 const newAccesesToken = jsonResponse.access_token; 27 const newScope = jsonResponse.scope; 28 pm.collectionVariables.set(\u0026#34;access_token\u0026#34;, newAccesesToken); 29 pm.collectionVariables.set(\u0026#34;scope\u0026#34;, newScope); 30}); Feedback Welcome Do you use Postman also? Feel free to share some tips and tricks you've found helpful, or leave a link to any of your pre/post request scripts that you'd like to share!\n","link":"https://blog.terakedis.dev/post/postman-pre-request-script-for-docebo-api/","section":"post","tags":["JSON","Postman","Docebo","automation"],"title":"Postman Pre-Request Script for Docebo API"},{"body":"I'm Robert Terakedis, a Principal Solutions Architect based in North Carolina. My background spans Azure cloud architecture, AI/LLM systems, enterprise automation, and about a decade of Apple platform work before that. I started in datacenter infrastructure at a bank, which taught me to think in terms of scale and operational reliability before I'd ever heard the term \u0026quot;cloud native.\u0026quot;\nMost recently at OneTrust, I've been building AI-powered automation systems in Python on Azure - things like document processing pipelines with vector embeddings, enterprise integrations between platforms that were never designed to talk to each other, and product analytics instrumentation. Before that, I was the Apple platform SME at VMware, where I built reference architectures and ran certification programs for enterprise engineers. That role is where I learned to write for a technical practitioner audience, which I've been doing ever since.\nI have a patent, which still surprises me a little when I say it out loud.\nWhat I work on now My current focus is AI and automation on Azure - specifically end-to-end Python pipelines using Azure OpenAI, AI Search, and Durable Functions. I'm interested in the architecture decisions that determine whether an AI-assisted system actually holds up under real-world conditions, not just in demos.\nI also think a lot about privacy and compliance as architectural constraints. It's not a separate concern you bolt on at the end. I hold the CIPP/E and CIPM certifications, and that background shapes how I approach system design.\nHow I work I build with Python for new projects. Cloud work is on Azure. Daily tools: VS Code, Git, Postman, Draw.io. I've shipped integrations with Jira Service Management, Salesforce, Docebo, Confluence, and Gainsight PX.\nThis site runs on Hugo, Cloudflare Pages, and GitHub.\nYou can find me on LinkedIn or just browse the blog to see what I've been thinking about lately.\nBackground Dual-degree from Kent State (Management \u0026amp; Information Systems + Integrated Life Sciences). Grew up in small-town Ohio. Now in North Carolina with my family, probably spending too much time building Lego sets with my kids.\n","link":"https://blog.terakedis.dev/about/","section":"page","tags":null,"title":"About me"},{"body":"","link":"https://blog.terakedis.dev/page/","section":"page","tags":null,"title":"Pages"},{"body":"What’s here This page pulls together my writing on the topics I care most about right now: Azure cloud architecture, AI/LLM systems, Python automation, and systems thinking.\nI write through blog posts rather than static samples because posts are more honest - they show how I actually approached a problem, including the parts that didn’t work the first time. If you want to get a sense of how I think, the posts below are a better signal than a resume bullet.\nLooking for something specific? Browse the full blog or check out the About page for my background.\nRecent posts on Azure, AI, and architecture Posts tagged: azure ai llm python automation architecture\nazure 2026-04-18: The $300K Spreadsheet: How Manual Provisioning Became a Liability automation 2026-04-18: The $300K Spreadsheet: How Manual Provisioning Became a Liability 2025-01-22: Postman Pre-Request Script for Docebo API 2020-10-27: Updated GitHub Actions to Publish Hugo Site From Private to Public Repo 2020-08-14: Using GitHub Actions to Publish Hugo Site From Private to Public Repo 2020-03-06: Another GitHub Actions Update - Using Deploy Keys Instead Of Personal Access Tokens architecture 2026-05-15: SSO Is Not a Flip Switch 2026-03-21: How to host blog images for free with Backblaze B2 and Cloudflare Want to talk through a systems or architecture problem? Find me on LinkedIn.\n","link":"https://blog.terakedis.dev/portfolio/","section":"page","tags":null,"title":"Portfolio"},{"body":"Below is a sample of recommendations from some of my awesome coworkers. For the full list of more than 35 recommendations, refer to my LinkedIn Profile!\nName Recommendation Rachel Kim I had the privilege of seeing Rob work a lot of his magic in a project we worked through since September 2023 until March 2024. He's got a humble demeanor but is a stacked and MVP-level contributor! While he honestly embodies elements of all the OneTrust values, the two I saw most prevalent and valuable to our project were 'Unlock Impact' and 'Step Up, Be Accountable. When coming up with the workflow for a new process, Rob embodied the value of 'Unlock Impact' by proactively reaching out to stakeholders to understand the needs and use cases, taking them into action by creating a process flow diagram for everyone to align on, and delighting all of our project team members with smooth next steps by working quickly with our Jira Admin on creating the Jira Service Management (JSM) form. His power to quickly 'Unlock Impact' actually started the engine and got us out of the garage - in retrospect, I am mighty grateful for it. Throughout the project, Rob also embodied the spirit of 'Step Up, be Accountable.' From start to finish, Rob functioned like a real team player, where any job undone was his responsibility. It can be difficult to balance that with doing everyone's job, but he know when to delegate (or nicely ask) SMEs to specific work. When defining the process workflow and creating a lot of the initial forms and content for the JSM portal, Rob helped clearly define who owns what in each step of the process and questioned band-aid type solutions for more scalable/sustainable options. These behaviors kept us on track and helped us work more cohesively as a team since folks knew what their jobs were as part of the greater project. This was crucial given our project team spans multiple functional areas, including Product, Engineering, CS, PS, Support, and Partners. Keeley Lucraft I had the pleasure of working with Rob on a high priority project at OneTrust. Rob’s technical knowledge, ability to document processes and thoroughness is extremely commendable. Rob has a fantastic ability (and patience) to explain technical aspects of systems in a way that someone who is ‘not so technical’ would understand. I’ll forever be grateful for the knowledge I obtained and learned whilst working alongside, Rob! He’s a rockstar in his field! Sonja Rissland I had the pleasure of working with Rob during my time at VMware. Not only is Rob extremely knowledgeable about the VMware ecosystem and the role of Mac within it, he's also always eager to help out, answer questions, and otherwise share that knowledge. Rob works well within diverse teams across organizational segments and verticals to deliver an optimal experience for customers. Adam Matthews Rob's ability to fully understand a complex platform, break it down into manageable parts and then deliver enablement for each piece is unrivalled. I consider myself incredibly fortunate to be able to call upon Rob for advice and direction, and the quality of his work makes me strive to continuously improve. The passion Rob puts into all of his work benefits the community as a whole and he is a fantastic colleague to work with. James Murray Rob is a real Apple (iOS, macOS, and tvOS) guru. In his role in Technical Marketing he has proved to be invaluable in enabling the WW EUC Solution Engineers to advise customers on the benefits of using Workspace ONE for configuring and managing their Apple devices. He produces high-quality presentations, blogs, and podcasts as well as always being keen to answer any questions we may have. He truly is an amazing team player. Joe McDonald Having worked with Rob for just under two years, I couldn’t be more grateful to have met someone. Rob’s kind and friendly nature make him easy to approach, coupled with his knowledge and quick responses to questions he is an invaluable member of our team. The material that Rob produces is widely used not just within our own organisation but across many, making him a trusted member across multiple communities. Jeff Harris I worked with Robert first as a peer and later as his direct manager. Robert has been an absolute pleasure to work with. From day one he was engaged and driven to learn any technology he could get his hands on. When we first began working together we were a small team and stretched thin and Rob stepped up to take on responsibilities outside his area of expertise with such determination that he would eventually go on to take full responsibility as a subject matter expert. He is the kind of individual whose absence is immediately felt when he is not in the office. Rob is always thinking of others and strives to improve himself and those around him. He listens to customer and peer issues intently, does his research and returns with well thought out and detailed responses, anticipating most questions that may arise and answering those at the same time. I have complete trust when assigning Robert a task and have thoroughly enjoyed working with him. Without hesitation I would recommend him in any technology or people related role. ","link":"https://blog.terakedis.dev/recommendations/","section":"page","tags":["Career"],"title":"Recommendations"},{"body":"","link":"https://blog.terakedis.dev/tags/apple/","section":"tags","tags":null,"title":"Apple"},{"body":"Sudden panic set in as I walked by my child's room and heard the lyric from Monsters by All Time Low -- \u0026quot;I don't mind while you f**k up my life.\u0026quot; Yes, I just had the realization that the critical parental controls on the Homepod Mini's in my children's rooms were no longer working as intended. This set off a multi-hour troubleshooting session, with some involvement from AppleCare, that got things working until it didn't (again). As it usually goes, \u0026quot;It Just Works\u0026quot;... until it doesn't.\nRudimentary Parental Controls for HomePod Mini Before digging too deep, it's important to understand one critical item. At the time of writing this blog, HomePods don't adhere to any \u0026quot;Screen Time\u0026quot; settings that you might have configured for other devices in your house (iPad, iPhone, etc). W\nThis lack of integration leaves you managing the HomePod Mini's configuration in the Home app. A few items that come into play when setting up a HomePod Mini for children (assuming things work as required):\nEach child should have their own Apple ID. See Create an Apple ID for your Child The child's Apple ID should be part of your Family --\u0026gt; This allows you to share the services (such as Music) in your Apple subscription (such as Apple One) Within the Home app, you should ensure the children are invited to participate in the Home (under Home Settings \u0026gt; People) When clicking on each child (under Home Settings \u0026gt; People), ensure the Allow Explicit Content setting is disabled under the Music \u0026amp; Podcasts header If you tap-and-hold on a child's homepod in the Home app, click Accessory Details and then click the Gear icon. Under Music \u0026amp; Podcasts, ensure the Primary User is set to the child's ID. Optionally, you can enable \u0026quot;Personal Requests\u0026quot; to help Siri recognize your children's requests as diffrent from your own. I have this enabled for our Home (after my troubleshooting session with AppleCare) My Troubleshooting Steps What about the old unplug, replug? IT 101, right? Tried it. No Luck. As it happens, I tried doing a few different things to help resolve this:\nPower Cycling No Change Changing the HomePod's \u0026quot;Primary User\u0026quot; to \u0026quot;HomePod Account\u0026quot; and then logging in the HomePod account as one of the child Apple ID's. Still Getting Explicit Music regardless of whether the Siri request comes from a child or Me Removing the Homepods from the home, Resetting, and then Re-Adding the HomePod Minis. Then, prior to making any Siri requests, I opened the Home app on my children's iPads (logged in as their Apple ID) and helped them train Siri to recognize their voices. This seemed to work... SUCCESS! Or so we thought... iOS 16.2 Upgrade for HomePod Mini drops and all the devices get updated. Initially, getting errors that Siri can't find music in our Apple Music Library... AARRRGH! Try removing, resetting, reconfiguring the HomePod Minis -- Yay, we have Apple Music playing again! Getting Explicit Music regardless of whether the Siri request comes from a child, Me, or Google Translate's text-to-speech (added as a previously Unknown \u0026quot;Control\u0026quot; voice)... ARRRGH! Approximately three weeks later, getting the errors again that Siri can't find music in our Apple Music Library Try removing, resetting, reconfiguring the HomePod Minis -- Yay, we have Apple Music playing again! Getting Explicit Music regardless of whether the Siri request comes from a child, Me, or Google Translate's text-to-speech (added as a previously Unknown \u0026quot;Control\u0026quot; voice)... ARRRGH! Odd Behaviors with the Explicit Content settings. As it currently stands, both homepod minis are configured for child Apple ID's as the Primary User. Within the settings for Home \u0026gt; People, both Child Apple IDs are set to disallow Explicit Content. However, it seems the only setting determining whether Explicit content plays on those devices is whether or not my own Apple ID is configured to disallow Explicit Content in the Home app's People settings. Whaaaaat?\nI guess this means it's time to yet again see how long these HomePod Minis stay connected to my account in Apple Music.\nShare your Frustration? I'm curious to know if anyone else is seing similar issues with HomePod Minis -- dropping connection to the Apple Music Library, or not honoring Explicit Content settings? If so, let me know in the comments below!\n","link":"https://blog.terakedis.dev/post/homepod-mini-ignoring-family-explicit-content-settings/","section":"post","tags":["Apple"],"title":"HomePod Mini Ignoring Family Explicit Content Settings"},{"body":"Let me start by saying that I'm assuming many of you out there are in a situation quite similar to my own. This situation; having a personal Mac and a work Mac (and maybe an iPad Pro thrown into that mix also). But if you're also like me, you have limited space on your desk and don't want to be surrounded by monitors! This leads me to my most potent complaint about Apple hardware; the lack of a KVM switch.\nBut Universal Control, Right? Wrong. While I agree Universal Control is an amazing product, it is only useful if all your devices have a monitor attached. Universal Control is exactly that; a way to allow a single set of control devices (i.e. keyboard and mouse) to work magically across multiple computing devices. Yet, the extent of this magic from a Video perspective is simply that it helps discover device location to make the \u0026quot;hop\u0026quot; of your control devices across each video output (i.e. moving from monitor to monitor).\nLiving like a Luddite So here I sit, using my TE Smart 4-port Single-Monitor KVM switch. For the most part, it does what I need by allowing me to share a single monitor across an iPad Pro (personal), mac Mini (personal), and MacBook Pro (work). But, much like the KVMs I used in datacenters 20 years ago, it's ... temperamental. Periodically, a connected device forgets the monitor resolution. Or, sometimes a connected device doesn't see a control device (keyboard or mouse) when re-connecting to control. At that point, troubleshooting involves a myriad of turning the KVM off/on, restarting devices, etc. Overall, it works, but not without a degree of occasional frustration.\nY U NO Make KVM? When I saw the Apple Studio display released, I was excited. A great display with some excellent features (video and sound) built in. And with nearly the equivalent of an iPhone baked into the hardware package, I had to ask, why couldn't there have been KVM capabilities there? Think about it, why couldn't universal control be software-based in the monitor? Could you plug (or pair) the keyboard and mouse to some form of studio display that then recognized a keystroke combo (or universal control gestures) to switch between attached video sources? I dare to dream. But seriously, I think this is a missed opportunity from Apple.\nFeel free to leave your thoughts below...\n","link":"https://blog.terakedis.dev/post/apples-missing-hardware-kvm-switch/","section":"post","tags":["macOS","Apple","iPadOS"],"title":"Apple's Missing Hardware - A KVM Switch"},{"body":"","link":"https://blog.terakedis.dev/tags/ipados/","section":"tags","tags":null,"title":"IPadOS"},{"body":"","link":"https://blog.terakedis.dev/tags/macos/","section":"tags","tags":null,"title":"MacOS"},{"body":"","link":"https://blog.terakedis.dev/tags/brick-sketches/","section":"tags","tags":null,"title":"Brick-Sketches"},{"body":"","link":"https://blog.terakedis.dev/tags/free-downloads/","section":"tags","tags":null,"title":"Free-Downloads"},{"body":"","link":"https://blog.terakedis.dev/categories/lego/","section":"categories","tags":null,"title":"Lego"},{"body":"I was a total \u0026quot;builder\u0026quot; when I was young. I loved pretty much anything that let me create my own structures: Construx, Lego, Lincoln Logs, Marble Runs, etc. Looking back, it really ended up being Lego that I most enjoyed playing with. Back in my Lego heyday, Magtron and Blacktron were the sets most captivating to me. I was fascinated by space (thanks grandparents for the trip to Kennedy Space Center). Some of my favorite sets (which I still have some of the pieces):\nBlacktron II: Aerial Invader Blacktron II: Alpha Centauri Outpost Blacktron II: Galactic Scout Blacktron II: Super Nova II M-tron: Celestial Forager M-tron: Stellar Recon Voyager At any rate, the point of this is to say that growing up, my Lego journey was entirely focused on space themes.\nGetting Back Into Lego as AFOL I hadn't touched my old Lego sets for over 20 years. Yet, as my kids got old enough, the Lego sets at \u0026quot;Grandma's House\u0026quot; became a fun new toy for them that I was happy to indulge with them. That timing happened to coincide with the start of the COVID-19 pandemic, and as I was stuck inside my house I was looking for new projects to stay occupied. As I was looking on the Lego website for gift ideas for my children, I stumbled onto the Star Wars theme section and my mind was BLOWN. My most favorite sci-fi movies (yes, I've even watched the cartoon series), now immortalized in plastic! I had just found the decor for my 100% Work-from-Home Office! I dipped my toe in as an adult fan of Lego (AFOL) by purchasing the Slave 1 (75243) and Tantive 4 (75244). I cannot tell you how much fun it was to put these together. The end results looked amazing, and since then I've displayed them with wall mounts from Ultimate Display Solutions.\nMy Collecting Rules Lego can get expensive, especially the licensed sets (Star Wars, Nintendo, Disney, Marvel, etc). For myself, I've developed a set of rules for how I buy Lego for myself.\nNever buy sets at full-price anywhere except the Lego Store (or Lego.com). If you pay full price, get the VIP points Hold out for VIP events if possible -- get double or triple VIP points (which you can later turn into dollars off an order) Watch for sales (at Target, Walmart, or Costco) -- it's rare, but sometimes you can snag something fun Be Picky -- Since I mostly use sets as displays, I try to look for sets that look unique or have some unique feature (or that will just look interesting on the wall) Stick to one theme as much as possible!! For me, it's mostly Star Wars. That last point is critical, but I admit I've branched out in a few cases. For instance, I've started buying the Winter Village sets to build with my children as a tradition. We place them out every christmas and the kids actually play with them periodically (it's fun to watch). I've also personally nabbed the Nintendo Game System set and Question Block, as well as the Ecto-1 and Delorean for childhood nostalgia.\nDiving into Bricklink Studio and Brick Sketches As I went deeper down the Lego rabbit hole, I learned about Bricklink (and Studio, which at the time didn't work on macOS Big Sur) and the fact that there's an entire marketplace of folks buying/selling/collecting Lego. This also led me to look at past years sets (many of which are out of my price range now) and the YouTube videos folks created talking about the builds. I'll admit, seeing all the sets I missed is a bit of a bummer. Death stars, Jawa Sandcralwers, etc.\nHowever, that research started getting me familiar with some of the alternate formats such as BrickHeadz and Brick Sketches. I took an interest in the Brick Sketches, which led me to discover the instagram feed for Chris McVeigh who designed a number of them. This gave me lots of ideas on possible sets and this format struck me as such a neat and creative way to artistically display my favorite star wars characters in an unobtrusive way. I also like that this format is easy to wall mount as it tends to be lightweight (due to size and brick count).\nI wanted to see if there was a way to start creating my own brick sketches. As I mentioned before, I display all my sets, so I don't have a pile of random bricks I can use. I decided to give Bricklink Studio another try as a way to possibly create some new sketches. I am happy to report that Studio now seems to be compliant with modern macOS security features (such as notarization). I set to work on going through the tutorial (nicely done by the way), and then started on a Brick Sketch template:\nDOWNLOAD NOW: Bricklink Studio Brick Sketch Frame Template\nAmazingly, it didn't take much longer than 30 minutes to go through the tutorial and then start creating the base template above. I hope it helps save you some time so you can get straight to designing. As I create my own sketches, I'm planning to post them up on BrickLink.\nWrapping It Up I'm enjoying this little dive into Bricklink Studio. I've found the controls fairly intuitive and enjoy how well-thought the on-screen behavior is when building. It definitely seems to prevent you from making silly mistakes and arranging bricks in places where they won't technically fit. Looking forward to seeing what you create using this template!\n","link":"https://blog.terakedis.dev/post/bricklink-studio-afol-brick-sketch-template/","section":"post","tags":["brick-sketches","free-downloads"],"title":"My First Dive into BrickLink Studio and a Brick Sketch Template"},{"body":"","link":"https://blog.terakedis.dev/tags/dns/","section":"tags","tags":null,"title":"DNS"},{"body":"I was listening to the Security Now Podcast the other day, and a GREAT topic came up that I wanted to write about. That topic, as covered by Leo and Steve, involves ownership (specifically registrants) of Domain Name System (DNS) records. I know from past experience, domain \u0026quot;ownership\u0026quot; never seems to be front-of-mind for many folks deciding to start a website. Whether you're simply looking to blog, or starting a new business, the focus tends to be on the outcome (\u0026quot;I have a new website\u0026quot;) than the path to get there. In this article, I want to cover some of the common ways domains are registered and how you can protect your domain name should something ever happen.\nWhy the Registrant is Important If you didn't already know, there is no such thing as domain \u0026quot;ownership.\u0026quot; You cannot simply buy a domain name outright and own it forever. Rather, domain names fall under a model more akin to leasing. While it can vary from registrar to registrar, the amount of time and money for which you lease your domain name can vary. Just like a lease for a car or apartment, the contact information associated with your domain name provides the details for who is responsible for maintaining and paying for that domain name. This contant information is known as the registrant (or \u0026quot;Registered Name Holder\u0026quot;). Typically, the registrant is the person that pays to \u0026quot;lease\u0026quot; the domain name at the registrar. However, if you are not the registrant AND you need to make changes to your domain name (such as pointing to a new web server or email server), you may run into problems.\nHere's where this usually goes awry:\nScenario 1: You contract with a developer (or web design firm) to create a website for your new business. The developer registers the domain to deliver the website. The relationship with your web developer sours (and they are no longer renewing your \u0026quot;lease\u0026quot; on the domain name). Or perhaps the web development company closes. Either way, you decide to hire someone new to make changes, but you didn't have access to your domain name configuration panel. Scenario 2: Your business partner registered the domain name with their personal details. Unfortunately, your partner suffers unexpected health problems making them incapable of making changes (or altering the domain ownership). So yes, you may have been using that domain name for years. But, if your name isn't anywhere in the registrar's records, you'll be gathering a LOT of paperwork to gain access.\nHow to avoid Registrant issues When it comes to registering domain names, there are a few best practices that can help avoid registrant issues in the future.\nUse a secure password (and MFA): When you set up an account with a reputable domain registrar (Cloudflare, Google, Amazon, etc), use a shared mailbox email address and secure password for both the mailbox and registar account. You do not want communications about the account going to a single person, nor do you want to tie the registrar account to a single person (that could eventually leave the company). Assign domain ownership to a Company: If the domain name is business-related, register the domain using the corporate entity. Specifically, make sure your organization's legal name is listed in the Registrant Organization field, and the role-based or department-based name is listed in the Registrant Name field. Optionally, you can use Domain Privacy (a feature at most registrars) to hide contact information about the domain. Register the domain name for the longest possible length: A common source of problems with domain names is that the registration (lease) expires and then the domain is purchased by someone else. Plan ahead with payment details: Set the domain to auto-renew and be sure to provide multiple forms of payment on the account. If you simply provide a single credit card number, you run the risk of the card number no longer working at renewal and the renewal failing. Provide backup contact information: This particular step helps ensure that more than one person can retrieve the domain account. This is particularly important if the email address used to originally register the domain name is no longer valid (e.g. employee leaves company). Of particular importance, ensure the backup contact email address uses a domain component different than the domain name you've registered. If the domain registration expires or gets suspended, there will be no way to get the backup emails if the backup email address uses the same domain name! As a side note, it's also important to lock your domain name after registering. This ensures that you don't run the risk of some unscrupulous people attempting to take over your domain by moving it to another registrar.\nFinal Thoughts Domain Name management always seems like a set-and-forget type of task, but a little preparation can ensure you minimize impact to your business in unforseen circumstances. Feel free to share any domain name horror stories or tips I may have missed in the comments below!\n","link":"https://blog.terakedis.dev/post/ensuring-domain-name-survival/","section":"post","tags":["Hugo","DNS"],"title":"Ensuring Domain Name Survival"},{"body":"","link":"https://blog.terakedis.dev/categories/learn-something-new/","section":"categories","tags":null,"title":"Learn Something New"},{"body":"Lots of changes afoot in my professional life. This past February, I joined the ranks of many folks in the Great Resignation and made a career shift. With my new role, I'm working for OneTrust -- a company that creates software to help other companies manage their privacy and regulatory compliance. It's been a total shock to the system --\u0026gt; completely new vocabulary, new verticals, everything. To say I'm feeling impostor syndrome would be an understatement, but I welcome the opportunity to get out of my comfort zone and learn something new.\nStarting a New Job It's been almost 8 years since I've had a \u0026quot;first day\u0026quot; at a new job. A few notes on what I've found useful:\nRead The First 90 Days -- it's a bit boring, but the content is a great reminder that much of your time when you first start the job should be to listen. Understand the culture, understand the important players, understand the problem you've been hired to solve. If certifications are required, start studying ASAP. The longer you're there, the deeper into the weeds you get and the less time you'll have for studying. Start as soon as you can. I also found it helped to set a date for my certification exams as it kept me motivated to study. Find opportunities to take on small tasks early. You'll meet new people, gain positive reputation and trust through quick wins, and add value while you on-board. Attend as many meetings as you can, but only as a listener. Planning sessions, triage sessions, anything. Be a fly on the wall, take note of struggles, technology problems, and personal interactions. This goes back to #1 in that you'll learn a lot by listening. What Am I Doing? In my new role, there's a number of small projects that we get to work on. However, one of the main focuses of the role is technical enablement for partners and customers. In this capacity, the team I'm on creates Hands-On Labs. Basically, we support, run, and develop content for OneTrust Hands-On Labs (or HOLs). This means developing a step-by-step walkthrough on how to achieve a common end-result using OneTrust software. Each HOL delivers a blank instance of OneTrust's SaaS product and an accompanying manual that demonstrates all the button-clicks and explanations.\nI particularly enjoy this part of my job, as I love the opportunity to teach people how to accomplish an end-goal. Also, it's fun to watch how HOLs evolve from \u0026quot;what is this thing?\u0026quot; to \u0026quot;high-demand sales tool\u0026quot;. In both my current role and previous role, this seemed to occur because of two reasons. First, HOLs allow prospects and customers to see how the product can solve a particular problem. Second, HOLs can become \u0026quot;playgrounds\u0026quot; for trying new ideas and testing something new (like a workflow, process, etc).\nI expect that over time, as my knowledge of the product and Privacy industry increases, I'll be working on more technical content as well (similar to what I did for VMware TechZone). I'm already working on a possible website to use for hosting that.\n** more to come **\nCurious? If you're thinking about starting down the path of a job in the Privacy space, feel free to reach out. Always happy to talk shop!\n","link":"https://blog.terakedis.dev/post/new-job-industry-privacy-onetrust-copy/","section":"post","tags":["Career","Privacy"],"title":"New Job in the Privacy Industry at OneTrust"},{"body":"Since I first started this blog, I was using a theme called BeautifulHugo. The theme was great and for the most part visually appealing. However, there were some changes I was hoping to see made to the theme that never really manifested. I had even contributed back some of my own changes, but it appeared the theme had been abandoned by it's creator. I set about looking for a new theme and stumbled onto hugo-clarity. Not only was this theme visually appealing to me, but I could see regular changes being made to the theme. Yes, a theme still under active maintenance!\nContributed Changes In my own blog, there were a few customizations I had made to BeautifulHugo that I wanted to move into hugo-clarity. Fortunately, with the theme under active maintenance, these changes were quickly merged in when I sent up the Pull Request in Github. Here's the few changes I added:\nChanges Made Description How to Use It Sidebar Disclaimer Adds the ability to display a disclaimer (i.e. \u0026quot;my views are not my employer's) in the sidebar. Documentation Adding support for Utterances Comments Adds support for the Utterances Github-based commenting system. It's similar to the commenting scheme used on docs.microsoft.com and helps filter comments by requiring the user is logged-in via a GitHub account. Documentation High Level Migration Steps Swapping Out the Theme This time around, I added the theme as a Git Submodule. I went this route as I wanted to be able to periodically pull in updates to the theme, and I also wanted to force myself to stop making edits directly in the theme directory.\nLesson Learned: I ran into a few instances with Beautiful Hugo where I upgraded the theme and overwrote some changes I had made directly in the theme's directory structure. With hugo-clarity (and going forward with any theme I try), the theme can be customized using a directory structure of configuration files OUTSIDE the theme's directory.\nWith the theme set up as a Git Submodule, I can now pull in updates directly from Git (without having to do any fancy copy/pasting) and test locally with the hugo binary (hugo serve) before publishing.\nMoving Configurations This was the most tedious part of the theme changeout. The themes used two totally different sets of TOML config files, and many of the configuration parameters had different names. To get started, I followed this process:\nCopied the config folder from themes/hugo-clarity/exampleSite/ into my main site's structure (/). Modified config/_default/config.toml and config/_default/params.toml to bring forward old values (from beautifulhugo's config.toml) into the new keys in the hugo-clarity site. Some important values include: baseURL, copyright, DefaultContentLanguage, publishDir, author Modified config/_default/menus/menu.en.toml to replicate the old menu structure from beautifulhugo. My Example menu file is as follows:\n1[[main]] 2 name = \u0026#34;Home\u0026#34; 3 url = \u0026#34;\u0026#34; 4 weight = -110 5 6# Submenus are done this way: parent -\u0026gt; identifier 7[[main]] 8 name = \u0026#34;About\u0026#34; 9 url = \u0026#34;/about/\u0026#34; 10 weight = -107 11 12[[main]] 13 name = \u0026#34;My Contributions\u0026#34; 14 identifier = \u0026#34;Links\u0026#34; 15 weight = -108 16[[main]] 17 parent = \u0026#34;Links\u0026#34; 18 name = \u0026#34;EUC TechZone\u0026#34; 19 url = \u0026#34;https://techzone.vmware.com/users/robert-terakedis\u0026#34; 20[[main]] 21 parent = \u0026#34;Links\u0026#34; 22 name = \u0026#34;VMware Tech Network\u0026#34; 23 url = \u0026#34;https://communities.vmware.com/people/rterakedis/activity\u0026#34; 24[[main]] 25 parent = \u0026#34;Links\u0026#34; 26 name = \u0026#34;VMware EUC Samples\u0026#34; 27 url = \u0026#34;https://github.com/vmware-samples/euc-samples/tree/master/macOS-Samples\u0026#34; 28[[main]] 29 parent = \u0026#34;Links\u0026#34; 30 name = \u0026#34;VMware EUC Blog\u0026#34; 31 url = \u0026#34;https://blogs.vmware.com/euc/author/rterakedis\u0026#34; 32 33# social menu links 34[[social]] 35 name = \u0026#34;github\u0026#34; 36 type = \u0026#34;social\u0026#34; 37 weight = 1 38 url = \u0026#34;https://github.com/rterakedis\u0026#34; 39[[social]] 40 name = \u0026#34;twitter\u0026#34; 41 weight = 2 42 url = \u0026#34;https://twitter.com/robterakedis\u0026#34; 43[[social]] 44 name = \u0026#34;linkedin\u0026#34; 45 weight = 3 46 url = \u0026#34;https://www.linkedin.com/in/terakedis\u0026#34; 47[[social]] 48 name = \u0026#34;rss\u0026#34; 49 weight = 4 50 url = \u0026#34;index.xml\u0026#34; Modify Front-Matter I'm fortunate that I didn't have a lot of pages, as one of the key steps in this was to reformat the front-matter on all my blog posts and pages. Fortunately, hugo-clarity has an archetype for new posts, meaning all I had to do was copy the front matter from that archetype into my current posts/pages and make some modifications. Fortunately, any time you use the hugo command to create a new page or post (hugo new posts/my-first-post.md), it'll automatically fill with the new front matter.\nClosing Commentary As always, comments are welcome. My blog always seems to be a work in progress, but so far I'm loving the fonts and colors chosen by the theme's designers. I've also spent some time running this theme through Google's PageSpeed tests and it's far more responsive than BeautifulHugo. Hopefully you enjoy it as much as I do.\n","link":"https://blog.terakedis.dev/post/change-from-beautifulhugo-to-clarity-theme/","section":"post","tags":["GitHub","Hugo"],"title":"Change From Beautifulhugo to Clarity Theme"},{"body":"","link":"https://blog.terakedis.dev/tags/github/","section":"tags","tags":null,"title":"GitHub"},{"body":"","link":"https://blog.terakedis.dev/categories/hugo/","section":"categories","tags":null,"title":"Hugo"},{"body":"Since Workspace ONE UEM version 9.3, VMware has included the open-source munki binaries in the Workspace ONE Intelligent Hub for macOS. The intent of this integration was to give macOS admins the ability to distribute 3rd-party non-store macOS apps, without the need to host any backing instrastructure for munki. Because this integration was meant to give admins some of the commonly used functionality, it was not integrated in such a way to include support for ALL munki's functionality. This has left some confusion in the community, particularly with Workspace ONE UEM administrators with extensive munki knowledge/background.\nPurpose I frequently see questions about what munki functionality works in Workspace ONE UEM. I'm hoping this post helps lay out some common usage parameters in munki, and how they equate to functionality in Workspace ONE UEM. If you identify a gap (due to new/existing munki functionality), or a new way that workspace ONE handles particular functionality, please leave a comment below!\nConverting \u0026quot;munki\u0026quot; Functionality/Terminology to Workspace ONE UEM First off it's helpful to understand how some of the common components to munki map to elements inside Workspace ONE UEM.\nMunki \u0026quot;Infrastructure\u0026quot; Munki Component Workspace ONE Equivalent Catalogs Workspace ONE Intelligent Hub dynamically builds this device-side based on assigned Apps Manifests Workspace ONE Intelligent Hub dynamically builds this device-side based on assigned Apps Managed Software Center Workspace ONE Intelligent Hub's App Catalog replaces this tool Repository Workspace ONE UEM acts as the repository, hosting all installable files securely in Workspace ONE UEM (On-Premises) or Content Delivery Network (SaaS) Autopkg(r) While not exactly a munki components, many folks use this in conjunction with their munki environment. There's currently no processor to provide direct integration with Workspace ONE UEM. That said, you can use the \u0026quot;munki\u0026quot; version of many recipes to obtain the binary and pkginfo.plist for upload into Workspace ONE UEM. Additionally, the recent version of Workspace ONE Admin Assistant includes command line functionality to upload binaries and pkginfo files to Workspace ONE UEM (if you're attempting to script an entire workflow). Munki pkginfo Files Now that you understand the back-end components of Munki and how they equate in Workspace ONE UEM, let's look at the actual pkginfo file that is used to build the catalogs \u0026amp; manifests.\nPkginfo Key Mapping that key to Workspace ONE UEM autoremove Remove on Unenroll setting in “Restrictions” for App Assignment blocking_applications Blocking Apps in App Configuration catalogs \u0026amp; manifests Smart Groups in App Assignments display_name Modify the name value in the plist generated by Workspace ONE Admin Assistant force_install_after_date Combine “Auto” deployment with “Installation Begins On” in App Assignment installable_condition \u0026amp; Conditional Items Smart Groups in App Assignments (or FreeStyle Orchestrator when released) installs, receipts Modify the plist generated by Workspace ONE Admin Assistant to add this key-value pair/array items_to_copy Supported if added to the pkginfo plist prior to uploading in Workspace ONE UEM - not currently displayed in UI installer_choices_xml Supported if added to the pkginfo plist prior to uploading in Workspace ONE UEM - not currently displayed in UI minimum_os_version \u0026amp; maximum_os_version Smart Groups in App Assignments _script (installcheck, pre/post, etc) Scripts tab in App Configuration _alert (preinstall, preuninstall, etc) Use hubcli in the relative “_script” in the Scripts tab in App Configuration requires Use the upcoming FreeStyle Orchestrator functionality to create a workflow with the required app installation order RestartAction Deployment tab in App Configuration supported_architectures Smart Groups in App Assignments uninstall_method Scripts tab in App Configuration Closing Commentary In my opinion, I've seen this integration help new-to-Workspace ONE UEM administrators quickly get macOS app deployment tamed in their environment. Especially from the perspective of the \u0026quot;accidental MacAdmin,\u0026quot; this integration brings macOS non-store app management into a similarly familiar look and feel to non-store app management on other platforms. If you are looking to do more comparison between the two app deployment systems, you can find more information at the following two resources:\nDeploying 3rd-Party Apps with Workspace ONE UEM Munki Wiki If you find a functionality gap that seems to be a blocker, I encourage you to add it to the Workspace ONE UEM Feature Request Portal.\nComments welcome!\n","link":"https://blog.terakedis.dev/post/mapping-concepts-from-munki-to-workspace-one-uem/","section":"post","tags":["macOS","Munki"],"title":"Mapping Concepts from Munki to WS1 UEM"},{"body":"","link":"https://blog.terakedis.dev/tags/munki/","section":"tags","tags":null,"title":"Munki"},{"body":"","link":"https://blog.terakedis.dev/categories/workspace-one/","section":"categories","tags":null,"title":"Workspace ONE"},{"body":"I was recently reading a post about What to do when you have to lay off your Jamf administrator, and it got me thinking. The Workspace ONE UEM documentation generally specifies what you need from a software and hardware perspective in pre-requisites. That said, over the years I've come to know a few unwritten (or written but obscure) best practices for setting up Workspace ONE UEM to manage Apple devices. Hopefully you find this post helpful, but I welcome any comments and feedback!\nBest Practices for Apple Services Pre-Requisites When your organization pursues the intiial setup for many of Apple's services, there are some best practices to follow which helps maintain supportability in unforseen circumstances. Additionally, many of the accounts you create to manage Apple-related services should be dedicated for that specific service. In other words, an Apple ID created for push certificate creation should not be used to sign-in to the App Store or iCloud on an iPhone. That said, let's dig into some of the critical best practices that I've seen over the years.\nAdmin Apple ID's for ABM (ASM) Apple calls out a number of pertinent pre-requisites for Administrator Apple ID's in ABM/ASM that are worth noting:\nThe email address hasn't been used as an Apple ID for any other Apple service, website, App Store, or iCloud content. This email address DOES need to route to a real person as it will receive critical notifications about ABM/ASM. Because Apple wants this initial admin account to be a real person, I highly recommend signing up using an alias email address (or distribution list email address) for your real org-based email address. By doing this, you need not worry if you've already used your org-based email address as an Apple ID in other services. Additionally, should you decide to leave the organization, the alias can be moved to someone else's mailbox (or one or more new email addresses added to the Distribution List). The idea here is to make business continuity easier. The Distribution List option also adds flexibility if you have other administrators that would benefit from awareness of program and software license agreement changes.\nNOTE: Use an email alias and NOT a shared mailbox, as this eliminates any overhead related to managing the additional mailbox and keeps ABM-related emails front-and-center.\nOnce your Admin ID is created and you have access to ABM (or ASM), set up additional administrators immediately and work with those individuals to ensure they go through the entire setup process (including 2-factor authentication). Also, set a recurring calendar reminder for your group of administrators to validate logins/credentials are working if they will not be actively managing ABM. If an organization loses access to all the administrator accounts, it can be painful to re-establish access and could potentially involve downtime related to Apple Business Manager programs (automated enrollment and volume licensed apps).\nPush Certificates Portal Admin Apple ID Push Certificates (used for APNS messaging), are unfortunately tied to a single Apple ID. Once a device is enrolled using an APNS push certificate, that certificate chain must be maintained (renewed) for continued functioning AND cannot be changed without re-enrolling the device. In other words, if you lose access to the account that created the Push Certificate, eventually your devices stop responding to MDM. Because of the criticality of this piece, DO NOT tie this Apple ID to a single Admin user.\nPrior to setting up Push Certificates, create a new Apple ID at appleid.apple.com using an email alias for a Distribution List or Group in your organization. This distribution list should contain all the users in your organization that will be managing MDM (which may or maynot be the same as those folks added as admins for ABM/ASM) Additionally, work with your co-administrators to immediately configure a set of Trusted Phone Numbers. Yes, you should have multiple trusted phone numbers just in case of emergency! Then, ensure everyone knows how to login to the Push Certificates Portal and keep the password in a shared password vault.\nREMEMBER: The crticial point with the Push Certificates Apple ID is that you need access to that account in order to periodically renew your APNS certificates. Without this capability, you'll lose the ability to manage your devices.\nLocations and MDM Servers in Apple Business Manager Locations and MDM Servers in ABM equate to a container holding volume-purchased app/book licenses and devices (respectively) for management. Over the years, I've known organizations which aimed to keep the number of MDM servers or Locations small as possible. To be honest, I never understood this thought process (or the organizational politics that discouraged liberal use of this ABM feature). I encourage you to create as many as you need to adequately test new configurations. With ABM, you can move devices and licenses easily between different Locations/Servers, allowing you the following opportunities:\nTesting new configurations in a Sibling (same level) Organization group Testing new configurations in a Child Organization group (overriding or replacing) Using a spare paid (non-free) license for testing in a UAT environment Purchasing additional \u0026quot;Free\u0026quot; licenses for testing in a UAT environment My point is this, Apple gives you a great deal of flexibility to move devices and app/book licenses around. Combine this with the multi-tenancy (Org-Group Heirarchy) of Workspace ONE and you get a great deal of flexibility for testing app or device deployment changes without \u0026quot;testing in production\u0026quot;. That is, unless you firmly believe in YOLO'ing your UEM environment... (NOT recommended!)\nBest Practices for Workspace ONE Define Multiple Customer-Level Administrators As a best practice, you should never have a single UEM admin user at the top of your Workspace ONE org structure. I would recommend having at least two (one of which is the manager of the UEM Admins) and if possible keep them geographically distributed (for business continuity). That said, having folks with access isn't enough. Create documentation on how to revoke admin privileges and create new admin accounts. Keep this documentation readily available in case of emergency (or if you have a place to store business continuity documention, keep it there).\nDefine Additional Support Contacts Work with your sales reps to ensure there are multiple contacts in your organization that can open support tickets. You don't want to wait for a major outage to then discover there's noone listed as a valid support contact with VMware support.\nDefine a Basic UEM Admin Recovery Account It may be tempting to force all your admins to log in with corporate directory-based credentials, but it is critical to maintain at least one basic (non-directory) account in UEM. If you experience an issue with your directory integration, you would not be able to make any corrections to the configuration in UEM without a non-directory user available.\nREMEMBER: The Basic UEM admin account should still point to a real email address (or distribution list) in the case of password resets and notifications.\nEncourage Custom Apps for iOS If you're an organization that develops enterprise iOS apps and signs them with Enterprise (In-House) signing certificates, I strongly recommend moving to Custom Apps. I've seen many times in the past where an organization runs into a fire-drill when the provisioning package for their Enterprise iOS app expires and they need to find the correct iOS app developer who can create the new provisioning package for the UEM administrator. Custom Apps eliminate this headache. Yes, it may require some process changes for your developers and require some coordination with you for the app purchase in ABM, but it is the way forward.\nWrapping Up I'm sure over time I'll think of more to add to this list, but I wanted to make sure I got some of these ideas in my head down on \u0026quot;paper.\u0026quot; Please feel free to add a comment below if you think I've missed something.\n","link":"https://blog.terakedis.dev/post/best-practices-for-apple-admins-in-workspace-one-uem/","section":"post","tags":["macOS","Apple"],"title":"Best Practices for Apple Admins in Workspace ONE UEM"},{"body":"Source control used to be a scary, odd thing for me back in college and early in my career. Branching, merging, commits... all the buzzwords I remember from my early days dealing with Microsoft Team Foundation Server (TFS) and Subversion. Admittedly, I never felt like I spent the time required to understand how it all worked. Fast forward to mid-2017, when a group of us within VMware needed to share some of our tips and tricks with the broader VMware community. The EUC-Samples repository was born on GitHub, and my journey like Alice down the rabbit hole began. Over the years, as we've encouraged participation and knowledge sharing amongst those within the community, I still see a hesitation to learn git/GitHub and contribute to the repository.\nMy Inspiration For This Post Before 2017, I was YOU! It's been a personal journey to feel comfortable learning to use GitHub and sharing with the VMware community. I've come across numerous examples of blog posts, training videos, and more. Yet, I felt it time to talk through the specific tools that have helped me on my journey. I hope you find these tools equally valuable and find the confidence to share your knowledge and experience with the community.\nGetting Started with GitHub This year's Microsoft Ignite conference had a few great resources to introduce GitHub to new contributors within the community. Before checking them out, I recommend watching the video \u0026quot;What is Version Control\u0026quot; to understand the problem GitHub (or git in general) is solving. My inspiration for listing the following videos are that they're short and to the point. Additionally, they cover using GUI tools. These videos get you the knowledge you need to start working with Github repositories and contributing to basic projects.\nIn my own experience and interactions, folks new to GitHub don't want to dive straight into the command line to start interacting with repositories. I've tried to lean more towards videos that explain using visuals and GUI tools.\nGithub for Beginners: https://myignite.microsoft.com/sessions/efbd16c0-6f0e-427e-aee6-e8be6e5096ec?source=sessions Intro to GitHub: https://myignite.microsoft.com/sessions/b1fd3357-8e0e-45e2-bf95-f5f115d6406b?source=sessions VS Code: https://myignite.microsoft.com/sessions/23dabb00-fb0b-4df0-bfbf-b87fbb98009a?source=sessions Getting Better with GitHub As you grow comfortable cloning repos and generating commits to the repo, you'll need to start digging into pull requests and branching/merging. Something I learned (admittedly) late was the value of branches in the code creation process. Branches provide a useful way for you to keep a local copy that mirrors the repo, and then selectively test different changes by commiting against different branches. Personally, I've found branches a useful way for me to contribute changes back to community projects as it let's me isolate very specific (feature or bug-related) changes and then submit the changes to the upstream repo via pull requests.\nVersion Control Workflow Cheat Sheet: https://www.git-tower.com/learn/cheat-sheets/vcs-workflow/ Comparing Version Control Workflows: https://www.atlassian.com/git/tutorials/comparing-workflows Git First Aid Kit (Videos): https://www.git-tower.com/learn/git/first-aid-kit/ Working with GitHub in VS Code: https://code.visualstudio.com/docs/editor/github Visual Studio Code Help VS Code Cheat Sheet: https://www.git-tower.com/learn/cheat-sheets/vscode/ Getting Started with Visual Studio Code: https://code.visualstudio.com/docs/introvideos/basics Tools for GitHub and Scripting Personally, I made the switch to Visual Studio Code shortly after it was first introduced. The developer community is constantly adding new features (via extensions), and I find the code language support is awesome for much of what I do (scripting for system administration). That said, here's a list of the smattering of tools I use for GitHub and working with repositories/content:\nVisual Studio Code - unparalleled feature set (syntax highlighting, linting, built-in git client, and markdown preview) Tower Git Client - excellent, visual git client Working Copy -- git client for iPadOS iA Writer -- excellent markdown author tool for iPadOS Wrapping Up Have you found any great resources for learning about Git \u0026amp; GitHub? I'd love to add them! Leave a comment below and let me know!\n","link":"https://blog.terakedis.dev/post/great-free-resources-to-learn-github/","section":"post","tags":["GitHub"],"title":"Great *Free* Resources to Learn GitHub"},{"body":"Periodically, I see app vendors providing custom JSON schema files to help build app-specific configuration profiles for MDM (specifically Jamf). Workspace ONE UEM supports app-specific configuration, but currently via Custom Settings in an XML format. While many vendors also suppply a custom mobileconfig file or Custom Settings dictionary that can be used with Workspace ONE UEM, I hope in this post to show how any Workspace ONE admin can manually convert a Custom Schema JSON file to Custom Settings XML.\nExample Custom JSON Schema First, let's start with an example of a typical custom JSON schema. The example below comes from cmdSecurity, the company that makes cmdReporter (recently purchased by Jamf). While the actual JSON content is lengthy and scary-looking, the bits you need to actually create Custom Settings XML are easy to pick out.\nCustom Schema JSON for cmdReporter 1{ 2 \u0026#34;title\u0026#34;: \u0026#34;cmdReporter Settings\u0026#34;, 3 \u0026#34;description\u0026#34;: \u0026#34;Domain: com.cmdsec.cmdreporter\u0026#34;, 4 \u0026#34;options\u0026#34;: { 5 \u0026#34;remove_empty_properties\u0026#34;: true 6 }, 7 \u0026#34;properties\u0026#34;: 8 { 9 \u0026#34;AuditLevel\u0026#34;: 10 { 11 \u0026#34;type\u0026#34;: \u0026#34;integer\u0026#34;, 12 \u0026#34;title\u0026#34;: \u0026#34;AuditLevel\u0026#34;, 13 \u0026#34;description\u0026#34;: \u0026#34;Log Verbosity Level. Recommended: 1 or 2\u0026#34;, 14 \u0026#34;enum\u0026#34;: [1, 2, 3], 15 \u0026#34;enum_titles\u0026#34;: [1, 2, 3], 16 \u0026#34;default\u0026#34;: 1 17 }, 18 \u0026#34;AuditEventLogVerboseMessages\u0026#34;: 19 { 20 \u0026#34;type\u0026#34;: \u0026#34;boolean\u0026#34;, 21 \u0026#34;title\u0026#34;: \u0026#34;AuditEventLogVerboseMessages\u0026#34;, 22 \u0026#34;default\u0026#34;: false, 23 \u0026#34;description\u0026#34;: \u0026#34;Log messages cmdReporter deems non-critical. Recommended: false\u0026#34; 24 }, 25 \u0026#34;AuditEventExcludedUsers\u0026#34;: 26 { 27 \u0026#34;type\u0026#34;: \u0026#34;array\u0026#34;, 28 \u0026#34;description\u0026#34;: \u0026#34;Users to exclude from audit logging. Recommended: None\u0026#34;, 29 \u0026#34;title\u0026#34;: \u0026#34;AuditEventExcludedUsers\u0026#34;, 30 \u0026#34;options\u0026#34;: 31 { 32 \u0026#34;infoText\u0026#34;: \u0026#34;Key: AllowedEmailDomains\u0026#34; 33 }, 34 \u0026#34;items\u0026#34;: 35 { 36 \u0026#34;type\u0026#34;: \u0026#34;string\u0026#34;, 37 \u0026#34;title\u0026#34;: \u0026#34;Excluded User\u0026#34;, 38 \u0026#34;options\u0026#34;: 39 { 40 \u0026#34;inputAttributes\u0026#34;: 41 { 42 \u0026#34;placeholder\u0026#34;: \u0026#34;dan\u0026#34; 43 } 44 } 45 } 46 }, 47 \u0026#34;AuditEventExcludedProcesses\u0026#34;: 48 { 49 \u0026#34;type\u0026#34;: \u0026#34;array\u0026#34;, 50 \u0026#34;title\u0026#34;: \u0026#34;AuditEventExcludedProcesses\u0026#34;, 51 \u0026#34;description\u0026#34;: \u0026#34;Applications or processes to exclude from logging. Recommended: Only other security software\u0026#34;, 52 \u0026#34;items\u0026#34;: 53 { 54 \u0026#34;type\u0026#34;: \u0026#34;string\u0026#34;, 55 \u0026#34;title\u0026#34;: \u0026#34;Excluded Processes\u0026#34;, 56 \u0026#34;options\u0026#34;: 57 { 58 \u0026#34;inputAttributes\u0026#34;: 59 { 60 \u0026#34;placeholder\u0026#34;: \u0026#34;/Applications/Security Tool.app\u0026#34; 61 } 62 } 63 } 64 }, 65 66 \u0026#34;LogRemoteEndpointEnabled\u0026#34;: 67 { 68 \u0026#34;type\u0026#34;: \u0026#34;boolean\u0026#34;, 69 \u0026#34;title\u0026#34;: \u0026#34;LogRemoteEndpointEnabled\u0026#34;, 70 \u0026#34;description\u0026#34;: \u0026#34;Master switch for sending logs to remote destinations. Recommended: true\u0026#34;, 71 \u0026#34;default\u0026#34;: true 72 }, 73 \u0026#34;LogRemoteEndpointURL\u0026#34;: 74 { 75 \u0026#34;type\u0026#34;: \u0026#34;string\u0026#34;, 76 \u0026#34;title\u0026#34;: \u0026#34;LogRemoteEndpointURL\u0026#34;, 77 \u0026#34;description\u0026#34;: \u0026#34;Full URL with port number where logs will be sent.\u0026#34;, 78 \u0026#34;options\u0026#34;: 79 { 80 \u0026#34;inputAttributes\u0026#34;: 81 { 82 \u0026#34;placeholder\u0026#34;: \u0026#34;https://company.splunk.server:443/services/collector/raw\u0026#34; 83 } 84 } 85 }, 86 \u0026#34;LogRemoteEndpointType\u0026#34;: 87 { 88 \u0026#34;type\u0026#34;: \u0026#34;string\u0026#34;, 89 \u0026#34;title\u0026#34;: \u0026#34;LogRemoteEndpointType\u0026#34;, 90 \u0026#34;description\u0026#34;: \u0026#34;Switch which configuration is used for remote transmission. LogRemoteEndpoint(value) are additional configurations. Recommended: Splunk\u0026#34;, 91 \u0026#34;enum\u0026#34;: [\u0026#34;Syslog\u0026#34;, \u0026#34;REST\u0026#34;, \u0026#34;Kafka\u0026#34;, \u0026#34;Splunk\u0026#34;, \u0026#34;TLS\u0026#34;, \u0026#34;AWSKinesis\u0026#34;], 92 \u0026#34;enum_titles\u0026#34;: [\u0026#34;Syslog\u0026#34;, \u0026#34;REST\u0026#34;, \u0026#34;Kafka\u0026#34;, \u0026#34;Splunk\u0026#34;, \u0026#34;TLS\u0026#34;, \u0026#34;AWSKinesis\u0026#34;], 93 \u0026#34;default\u0026#34;: \u0026#34;Splunk\u0026#34; 94 }, 95 \u0026#34;LogRemoteEndpointAWSKinesis\u0026#34;: 96 { 97 \u0026#34;type\u0026#34;: \u0026#34;object\u0026#34;, 98 \u0026#34;title\u0026#34;: \u0026#34;LogRemoteEndpointAWSKinesis\u0026#34;, 99 \u0026#34;description\u0026#34;: \u0026#34;Send logs directly to an AWS Kinesis stream\u0026#34;, 100 \u0026#34;properties\u0026#34;: 101 { 102 \u0026#34;AccessKeyID\u0026#34;: 103 { 104 \u0026#34;type\u0026#34;: \u0026#34;string\u0026#34;, 105 \u0026#34;title\u0026#34;: \u0026#34;AWS Access Key ID\u0026#34;, 106 \u0026#34;description\u0026#34;: \u0026#34;(Required) AWS Access Key ID\u0026#34;, 107 \u0026#34;options\u0026#34;: 108 { 109 \u0026#34;inputAttributes\u0026#34;: 110 { 111 \u0026#34;placeholder\u0026#34;: \u0026#34;EJNPQUNWGIJ...\u0026#34; 112 } 113 } 114 }, 115 \u0026#34;SecretKey\u0026#34;: 116 { 117 \u0026#34;type\u0026#34;: \u0026#34;string\u0026#34;, 118 \u0026#34;title\u0026#34;: \u0026#34;AWS Secret Key\u0026#34;, 119 \u0026#34;description\u0026#34;: \u0026#34;(Required) AWS Secret Key\u0026#34;, 120 \u0026#34;options\u0026#34;: 121 { 122 \u0026#34;inputAttributes\u0026#34;: 123 { 124 \u0026#34;placeholder\u0026#34;: \u0026#34;vOQd2pqNMyNR3...\u0026#34; 125 } 126 } 127 }, 128 \u0026#34;StreamName\u0026#34;: 129 { 130 \u0026#34;type\u0026#34;: \u0026#34;string\u0026#34;, 131 \u0026#34;title\u0026#34;: \u0026#34;AWS Kinesis Stream Name\u0026#34;, 132 \u0026#34;description\u0026#34;: \u0026#34;(Required) AWS Kinesis Stream Name (NOT ARN)\u0026#34;, 133 \u0026#34;options\u0026#34;: 134 { 135 \u0026#34;inputAttributes\u0026#34;: 136 { 137 \u0026#34;placeholder\u0026#34;: \u0026#34;cmdReporter\u0026#34; 138 } 139 } 140 }, 141 \u0026#34;Region\u0026#34;: 142 { 143 \u0026#34;type\u0026#34;: \u0026#34;string\u0026#34;, 144 \u0026#34;title\u0026#34;: \u0026#34;Region\u0026#34;, 145 \u0026#34;description\u0026#34;: \u0026#34;(Required)\u0026#34;, 146 \u0026#34;default\u0026#34;: \u0026#34;us-east-1\u0026#34; 147 } 148 } 149 }, 150 \u0026#34;LogRemoteEndpointKafka\u0026#34;: 151 { 152 \u0026#34;type\u0026#34;: \u0026#34;object\u0026#34;, 153 \u0026#34;title\u0026#34;: \u0026#34;LogRemoteEndpointKafka\u0026#34;, 154 \u0026#34;description\u0026#34;: \u0026#34;Configure certificate and topic for Apache Kafka\u0026#34;, 155 \u0026#34;properties\u0026#34;: 156 { 157 \u0026#34;TopicName\u0026#34;: 158 { 159 \u0026#34;type\u0026#34;: \u0026#34;string\u0026#34;, 160 \u0026#34;title\u0026#34;: \u0026#34;TopicName\u0026#34;, 161 \u0026#34;description\u0026#34;: \u0026#34;(Required) Kafka topic cmdReporter will publish to\u0026#34;, 162 \u0026#34;default\u0026#34;: \u0026#34;cmdReporter\u0026#34; 163 }, 164 \u0026#34;TLSServerCertificate\u0026#34;: 165 { 166 \u0026#34;type\u0026#34;: \u0026#34;array\u0026#34;, 167 \u0026#34;title\u0026#34;: \u0026#34;TLSServerCertificate\u0026#34;, 168 \u0026#34;description\u0026#34;: \u0026#34;(Required) Common names for server certificate trust chain.\u0026#34;, 169 \u0026#34;items\u0026#34;: 170 { 171 \u0026#34;type\u0026#34;: \u0026#34;string\u0026#34;, 172 \u0026#34;title\u0026#34;: \u0026#34;Certificate Common Name\u0026#34;, 173 \u0026#34;options\u0026#34;: 174 { 175 \u0026#34;infoText\u0026#34;: \u0026#34;Maps to Kafka setting ssl.ca.location\u0026#34;, 176 \u0026#34;inputAttributes\u0026#34;: 177 { 178 \u0026#34;placeholder\u0026#34;: \u0026#34;Apple Root CA - G2\u0026#34; 179 } 180 } 181 } 182 183 }, 184 \u0026#34;TLSClientCertificate\u0026#34;: 185 { 186 \u0026#34;type\u0026#34;: \u0026#34;array\u0026#34;, 187 \u0026#34;title\u0026#34;: \u0026#34;TLSClientCertificate\u0026#34;, 188 \u0026#34;description\u0026#34;: \u0026#34;(Optional) common name of client certificate in system keychain.\u0026#34;, 189 \u0026#34;options\u0026#34;: 190 { 191 \u0026#34;infoText\u0026#34;: \u0026#34;Maps to Kafka setting ssl.certificate.location\u0026#34;, 192 \u0026#34;inputAttributes\u0026#34;: 193 { 194 \u0026#34;placeholder\u0026#34;: \u0026#34;server_name.company.com\u0026#34; 195 } 196 } 197 }, 198 \u0026#34;TLSClientPrivateKey\u0026#34;: 199 { 200 \u0026#34;type\u0026#34;: \u0026#34;string\u0026#34;, 201 \u0026#34;title\u0026#34;: \u0026#34;TLSClientPrivateKey\u0026#34;, 202 \u0026#34;description\u0026#34;: \u0026#34;(Optional) PEM formatted client private key\u0026#34;, 203 \u0026#34;options\u0026#34;: 204 { 205 \u0026#34;infoText\u0026#34;: \u0026#34;Maps to Kafka setting ssl.key.location\u0026#34;, 206 \u0026#34;inputAttributes\u0026#34;: 207 { 208 \u0026#34;placeholder\u0026#34;: \u0026#34;-----BEGIN CERTIFICATE-----...\u0026#34; 209 } 210 } 211 } 212 } 213 }, 214 \u0026#34;LogRemoteEndpointREST\u0026#34;: 215 { 216 \u0026#34;type\u0026#34;: \u0026#34;object\u0026#34;, 217 \u0026#34;title\u0026#34;: \u0026#34;LogRemoteEndpointREST\u0026#34;, 218 \u0026#34;description\u0026#34;: \u0026#34;REST or Splunk HEC settings. PublicKeyHash applies to both methods.\u0026#34;, 219 \u0026#34;properties\u0026#34;: 220 { 221 \u0026#34;PublicKeyHash\u0026#34;: 222 { 223 \u0026#34;type\u0026#34;: \u0026#34;string\u0026#34;, 224 \u0026#34;title\u0026#34;: \u0026#34;PublicKeyHash\u0026#34;, 225 \u0026#34;description\u0026#34;: \u0026#34;(Required) REST or Splunk HEC API key\u0026#34; 226 } 227 } 228 }, 229 \u0026#34;LogRemoteEndpointTLS\u0026#34;: 230 { 231 \u0026#34;type\u0026#34;: \u0026#34;object\u0026#34;, 232 \u0026#34;title\u0026#34;: \u0026#34;LogRemoteEndpointTLS\u0026#34;, 233 \u0026#34;description\u0026#34;: \u0026#34;\u0026#34;, 234 \u0026#34;properties\u0026#34;: 235 { 236 \u0026#34;TLSServerCertificate\u0026#34;: 237 { 238 \u0026#34;type\u0026#34;: \u0026#34;array\u0026#34;, 239 \u0026#34;title\u0026#34;: \u0026#34;TLSServerCertificate\u0026#34;, 240 \u0026#34;description\u0026#34;: \u0026#34;(Required) Common names for server certificate trust chain.\u0026#34;, 241 \u0026#34;items\u0026#34;: 242 { 243 \u0026#34;type\u0026#34;: \u0026#34;string\u0026#34;, 244 \u0026#34;title\u0026#34;: \u0026#34;Certificate Common Name\u0026#34;, 245 \u0026#34;options\u0026#34;: 246 { 247 \u0026#34;inputAttributes\u0026#34;: 248 { 249 \u0026#34;placeholder\u0026#34;: \u0026#34;Apple Root CA - G2\u0026#34; 250 } 251 } 252 } 253 254 } 255 } 256 }, 257 \u0026#34;UnifiedLogPredicates\u0026#34;: 258 { 259 \u0026#34;type\u0026#34;: \u0026#34;array\u0026#34;, 260 \u0026#34;title\u0026#34;: \u0026#34;UnifiedLogPredicates\u0026#34;, 261 \u0026#34;description\u0026#34;: \u0026#34;Search terms that will be collected from the unified log systems.\u0026#34;, 262 \u0026#34;items\u0026#34;: 263 { 264 \u0026#34;type\u0026#34;: \u0026#34;string\u0026#34;, 265 \u0026#34;title\u0026#34;: \u0026#34;Predicates\u0026#34;, 266 \u0026#34;description\u0026#34;: \u0026#34;Search terms that will be collected from the unified log systems.\u0026#34;, 267 \u0026#34;options\u0026#34;: 268 { 269 \u0026#34;inputAttributes\u0026#34;: 270 { 271 \u0026#34;placeholder\u0026#34;: \u0026#34;(subsystem == \\\u0026#34;com.apple.securityd\\\u0026#34;)\u0026#34; 272 } 273 } 274 } 275 }, 276 \u0026#34;LicenseEmail\u0026#34;: 277 { 278 \u0026#34;type\u0026#34;: \u0026#34;string\u0026#34;, 279 \u0026#34;title\u0026#34;: \u0026#34;LicenseEmail\u0026#34;, 280 \u0026#34;description\u0026#34;: \u0026#34;(Required)\u0026#34;, 281 \u0026#34;placeholder\u0026#34;: \u0026#34;name@company.com\u0026#34; 282 }, 283 \u0026#34;LicenseExpirationDate\u0026#34;: 284 { 285 \u0026#34;type\u0026#34;: \u0026#34;string\u0026#34;, 286 \u0026#34;title\u0026#34;: \u0026#34;LicenseExpirationDate\u0026#34;, 287 \u0026#34;description\u0026#34;: \u0026#34;(Required) Format: 01/20/2020\u0026#34;, 288 \u0026#34;placeholder\u0026#34;: \u0026#34;01/20/2020\u0026#34; 289 }, 290 \u0026#34;LicenseKey\u0026#34;: 291 { 292 \u0026#34;type\u0026#34;: \u0026#34;string\u0026#34;, 293 \u0026#34;title\u0026#34;: \u0026#34;LicenseKey\u0026#34;, 294 \u0026#34;description\u0026#34;: \u0026#34;(Required)\u0026#34;, 295 \u0026#34;placeholder\u0026#34;: \u0026#34;asdfh38chdj...\u0026#34; 296 }, 297 \u0026#34;LicenseType\u0026#34;: 298 { 299 \u0026#34;type\u0026#34;: \u0026#34;string\u0026#34;, 300 \u0026#34;title\u0026#34;: \u0026#34;LicenseType\u0026#34;, 301 \u0026#34;enum\u0026#34;: [\u0026#34;Trial\u0026#34;, \u0026#34;Annual\u0026#34;], 302 \u0026#34;enum_titles\u0026#34;: [\u0026#34;Trial\u0026#34;, \u0026#34;Annual\u0026#34;], 303 \u0026#34;default\u0026#34;: \u0026#34;Annual\u0026#34;, 304 \u0026#34;description\u0026#34;: \u0026#34;(Required)\u0026#34; 305 }, 306 \u0026#34;LicenseVersion\u0026#34;: 307 { 308 \u0026#34;type\u0026#34;: \u0026#34;string\u0026#34;, 309 \u0026#34;title\u0026#34;: \u0026#34;LicenseVersion\u0026#34;, 310 \u0026#34;description\u0026#34;: \u0026#34;(Required) Always leave at 1\u0026#34;, 311 \u0026#34;default\u0026#34;: \u0026#34;1\u0026#34; 312 }, 313 \u0026#34;LogFileMaxNumberBackups\u0026#34;: 314 { 315 \u0026#34;type\u0026#34;: \u0026#34;integer\u0026#34;, 316 \u0026#34;title\u0026#34;: \u0026#34;LogFileMaxNumberBackups\u0026#34;, 317 \u0026#34;description\u0026#34;: \u0026#34;Maximum number of archived backups to keep before deleting oldest.\u0026#34;, 318 \u0026#34;default\u0026#34;: \u0026#34;10\u0026#34; 319 }, 320 \u0026#34;LogFileMaxSizeMegaBytes\u0026#34;: 321 { 322 \u0026#34;type\u0026#34;: \u0026#34;integer\u0026#34;, 323 \u0026#34;title\u0026#34;: \u0026#34;LogFileMaxSizeMegaBytes\u0026#34;, 324 \u0026#34;description\u0026#34;: \u0026#34;Maximum log file size before rotating.\u0026#34;, 325 \u0026#34;default\u0026#34;: \u0026#34;50\u0026#34; 326 }, 327 \u0026#34;LogFileOwnership\u0026#34;: 328 { 329 \u0026#34;type\u0026#34;: \u0026#34;string\u0026#34;, 330 \u0026#34;title\u0026#34;: \u0026#34;LogFileOwnership\u0026#34;, 331 \u0026#34;description\u0026#34;: \u0026#34;User and group ownership of log files\u0026#34;, 332 \u0026#34;default\u0026#34;: \u0026#34;root:wheel\u0026#34; 333 }, 334 \u0026#34;LogFilePermission\u0026#34;: 335 { 336 \u0026#34;type\u0026#34;: \u0026#34;string\u0026#34;, 337 \u0026#34;title\u0026#34;: \u0026#34;LogFilePermission\u0026#34;, 338 \u0026#34;description\u0026#34;: \u0026#34;Octal permissions for live and archived log files.\u0026#34;, 339 \u0026#34;default\u0026#34;: \u0026#34;644\u0026#34; 340 }, 341 \u0026#34;SecurityBaseline\u0026#34;: 342 { 343 \u0026#34;type\u0026#34;: \u0026#34;string\u0026#34;, 344 \u0026#34;title\u0026#34;: \u0026#34;SecurityBaseline\u0026#34;, 345 \u0026#34;description\u0026#34;: \u0026#34;Name of the security baseline to report on\u0026#34;, 346 \u0026#34;enum\u0026#34;: [\u0026#34;800-53_high\u0026#34;,\u0026#34;800-53_low\u0026#34;,\u0026#34;800-53_moderate\u0026#34;,\u0026#34;cnssi-1253\u0026#34;,\u0026#34;all_rules\u0026#34;], 347 \u0026#34;enum_titles\u0026#34;: [\u0026#34;NIST 800-53_high\u0026#34;,\u0026#34;NIST 800-53_low\u0026#34;,\u0026#34;NIST 800-53_moderate\u0026#34;,\u0026#34;cnssi-1253\u0026#34;,\u0026#34;all_rules\u0026#34;], 348 \u0026#34;default\u0026#34;: \u0026#34;all_rules\u0026#34; 349 }, 350 \u0026#34;SecurityBaselineReportingInterval\u0026#34;: 351 { 352 \u0026#34;type\u0026#34;: \u0026#34;integer\u0026#34;, 353 \u0026#34;title\u0026#34;: \u0026#34;SecurityBaselineReportingInterval\u0026#34;, 354 \u0026#34;description\u0026#34;: \u0026#34;Number of minutes between security baseline reports.\u0026#34;, 355 \u0026#34;default\u0026#34;: \u0026#34;720\u0026#34; 356 }, 357 \u0026#34;ProhibitedApps\u0026#34;: 358 { 359 \u0026#34;type\u0026#34;: \u0026#34;object\u0026#34;, 360 \u0026#34;title\u0026#34;: \u0026#34;ProhibitedApps\u0026#34;, 361 \u0026#34;description\u0026#34;: \u0026#34;Configure applications to be blocked from user sessions.\u0026#34;, 362 \u0026#34;properties\u0026#34;: 363 { 364 \u0026#34;PAExecutableNames\u0026#34;: 365 { 366 \u0026#34;type\u0026#34;: \u0026#34;array\u0026#34;, 367 \u0026#34;title\u0026#34;: \u0026#34;PAExecutableNames\u0026#34;, 368 \u0026#34;description\u0026#34;: \u0026#34;Process names to block from launching from a user session.\u0026#34;, 369 \u0026#34;items\u0026#34;: 370 { 371 \u0026#34;type\u0026#34;: \u0026#34;string\u0026#34;, 372 \u0026#34;title\u0026#34;: \u0026#34;Executable Name\u0026#34;, 373 \u0026#34;options\u0026#34;: 374 { 375 \u0026#34;inputAttributes\u0026#34;: 376 { 377 \u0026#34;placeholder\u0026#34;: \u0026#34;fdesetup\u0026#34; 378 } 379 } 380 } 381 382 }, 383 \u0026#34;PASigningIdentifiers\u0026#34;: 384 { 385 \u0026#34;type\u0026#34;: \u0026#34;array\u0026#34;, 386 \u0026#34;title\u0026#34;: \u0026#34;PASigningIdentifiers\u0026#34;, 387 \u0026#34;description\u0026#34;: \u0026#34;Process signing IDs to block from launching from a user session.\u0026#34;, 388 \u0026#34;items\u0026#34;: 389 { 390 \u0026#34;type\u0026#34;: \u0026#34;string\u0026#34;, 391 \u0026#34;title\u0026#34;: \u0026#34;Executable App Signing ID\u0026#34;, 392 \u0026#34;options\u0026#34;: 393 { 394 \u0026#34;inputAttributes\u0026#34;: 395 { 396 \u0026#34;placeholder\u0026#34;: \u0026#34;com.apple.fdesetup\u0026#34; 397 } 398 } 399 } 400 401 }, 402 \u0026#34;PATeamIdentifiers\u0026#34;: 403 { 404 \u0026#34;type\u0026#34;: \u0026#34;array\u0026#34;, 405 \u0026#34;title\u0026#34;: \u0026#34;PATeamIdentifiers\u0026#34;, 406 \u0026#34;description\u0026#34;: \u0026#34;Process team IDs to block from launching from a user session\u0026#34;, 407 \u0026#34;items\u0026#34;: 408 { 409 \u0026#34;type\u0026#34;: \u0026#34;string\u0026#34;, 410 \u0026#34;title\u0026#34;: \u0026#34;Executable App Signing ID\u0026#34;, 411 \u0026#34;options\u0026#34;: 412 { 413 \u0026#34;inputAttributes\u0026#34;: 414 { 415 \u0026#34;infoText\u0026#34;: \u0026#34;This is the team ID for Wireshark as an example.\u0026#34;, 416 \u0026#34;placeholder\u0026#34;: \u0026#34;7Z6EMTD2C6\u0026#34; 417 } 418 } 419 } 420 } 421 } 422 } 423 } 424} Yeah, there's a lot to unpack there. Or is there?\nStep 1 - The Custom Settings Template First, we start with the NSUserDefaults Custom Settings template. To this, we must modify the following:\nString Value for PayloadDisplayName (this can be whatever you want to show up in the Profiles preference pane) Sting Value for PayloadType (from the \u0026quot;Domain: \u0026quot; line in the schema - this is the App's defaults domain) String Value for PayloadUUID (any UUID created by typing uuidgen in Terminal.app) String Value for PayloadIdentifier (a combination of the PayloadType.UUID) 1\u0026lt;dict\u0026gt; 2 \u0026lt;key\u0026gt;PayloadDisplayName\u0026lt;/key\u0026gt; 3 \u0026lt;string\u0026gt;com.cmdsec.cmdreporter Settings\u0026lt;/string\u0026gt; 4 \u0026lt;key\u0026gt;PayloadEnabled\u0026lt;/key\u0026gt; 5 \u0026lt;true/\u0026gt; 6 \u0026lt;key\u0026gt;PayloadIdentifier\u0026lt;/key\u0026gt; 7 \u0026lt;string\u0026gt;com.cmdsec.cmdreporter.A216A1AD-6B96-497A-B6CF-3E167318B127\u0026lt;/string\u0026gt; 8 \u0026lt;key\u0026gt;PayloadType\u0026lt;/key\u0026gt; 9 \u0026lt;string\u0026gt;com.cmdsec.cmdreporter\u0026lt;/string\u0026gt; 10 \u0026lt;key\u0026gt;PayloadUUID\u0026lt;/key\u0026gt; 11 \u0026lt;string\u0026gt;A216A1AD-6B96-497A-B6CF-3E167318B127\u0026lt;/string\u0026gt; 12 \u0026lt;key\u0026gt;PayloadVersion\u0026lt;/key\u0026gt; 13 \u0026lt;integer\u0026gt;1\u0026lt;/integer\u0026gt; Step 2 - Adding the Custom Settings Key-Value Pairs Each Key-Value Pair in the Custom Settings XML results from a JSON Property and Type. Proceeding down the schema file, you'll use these JSON objects to build a list of key-value pairs. A few instructions to note:\nThe \u0026quot;properties\u0026quot;:{ key means you're about to see a list of \u0026lt;key\u0026gt; names. In other words, substitute \u0026quot;properties\u0026quot;:{ for the XML \u0026lt;dict\u0026gt;\u0026lt;/dict\u0026gt; nodes. An object type with multiple properties results in a \u0026lt;key\u0026gt; where the value is a \u0026lt;dict\u0026gt; of multiple key-value pairs. An array type with an options key and items types results in a \u0026lt;key\u0026gt; where the value is an \u0026lt;array\u0026gt; of \u0026lt;type\u0026gt; (e.g. one or more values). A string type with enum values requires the use of one of the specific enumerated values. You cannot use an arbitrary string value. Converting Object Types For example, take the property LogRemoteEndpointKafka:\n1 \u0026#34;LogRemoteEndpointKafka\u0026#34;: 2 { 3 \u0026#34;type\u0026#34;: \u0026#34;object\u0026#34;, 4 \u0026#34;title\u0026#34;: \u0026#34;LogRemoteEndpointKafka\u0026#34;, 5 \u0026#34;description\u0026#34;: \u0026#34;Configure certificate and topic for Apache Kafka\u0026#34;, 6 \u0026#34;properties\u0026#34;: 7 { 8 \u0026#34;TopicName\u0026#34;: 9 { 10 \u0026#34;type\u0026#34;: \u0026#34;string\u0026#34;, 11 \u0026#34;title\u0026#34;: \u0026#34;TopicName\u0026#34;, 12 \u0026#34;description\u0026#34;: \u0026#34;(Required) Kafka topic cmdReporter will publish to\u0026#34;, 13 \u0026#34;default\u0026#34;: \u0026#34;cmdReporter\u0026#34; 14 }, 15 \u0026#34;TLSServerCertificate\u0026#34;: 16 { 17 \u0026#34;type\u0026#34;: \u0026#34;array\u0026#34;, 18 \u0026#34;title\u0026#34;: \u0026#34;TLSServerCertificate\u0026#34;, 19 \u0026#34;description\u0026#34;: \u0026#34;(Required) Common names for server certificate trust chain.\u0026#34;, 20 \u0026#34;items\u0026#34;: 21 { 22 \u0026#34;type\u0026#34;: \u0026#34;string\u0026#34;, 23 \u0026#34;title\u0026#34;: \u0026#34;Certificate Common Name\u0026#34;, 24 \u0026#34;options\u0026#34;: 25 { 26 \u0026#34;infoText\u0026#34;: \u0026#34;Maps to Kafka setting ssl.ca.location\u0026#34;, 27 \u0026#34;inputAttributes\u0026#34;: 28 { 29 \u0026#34;placeholder\u0026#34;: \u0026#34;Apple Root CA - G2\u0026#34; 30 } 31 } 32 } 33 34 }, 35 \u0026#34;TLSClientCertificate\u0026#34;: 36 { 37 \u0026#34;type\u0026#34;: \u0026#34;array\u0026#34;, 38 \u0026#34;title\u0026#34;: \u0026#34;TLSClientCertificate\u0026#34;, 39 \u0026#34;description\u0026#34;: \u0026#34;(Optional) common name of client certificate in system keychain.\u0026#34;, 40 \u0026#34;options\u0026#34;: 41 { 42 \u0026#34;infoText\u0026#34;: \u0026#34;Maps to Kafka setting ssl.certificate.location\u0026#34;, 43 \u0026#34;inputAttributes\u0026#34;: 44 { 45 \u0026#34;placeholder\u0026#34;: \u0026#34;server_name.company.com\u0026#34; 46 } 47 } 48 }, 49 \u0026#34;TLSClientPrivateKey\u0026#34;: 50 { 51 \u0026#34;type\u0026#34;: \u0026#34;string\u0026#34;, 52 \u0026#34;title\u0026#34;: \u0026#34;TLSClientPrivateKey\u0026#34;, 53 \u0026#34;description\u0026#34;: \u0026#34;(Optional) PEM formatted client private key\u0026#34;, 54 \u0026#34;options\u0026#34;: 55 { 56 \u0026#34;infoText\u0026#34;: \u0026#34;Maps to Kafka setting ssl.key.location\u0026#34;, 57 \u0026#34;inputAttributes\u0026#34;: 58 { 59 \u0026#34;placeholder\u0026#34;: \u0026#34;-----BEGIN CERTIFICATE-----...\u0026#34; 60 } 61 } 62 } 63 } 64 }, The \u0026lt;key\u0026gt; comes from the JSON name (e.g. \u0026quot;LogRemoteEndpointKafka\u0026quot;:) but with leading and ending punctuation removed. As mentioned earlier, the \u0026quot;properties\u0026quot;:{ key is changed out for the XML \u0026lt;dict\u0026gt;\u0026lt;/dict\u0026gt; nodes. Finally, we build the remaining key-value pairs within the dictionary using each JSON name (as the key), and the placeholder value in each type (optionally as arrays).\nTo make it easier to read, here's the pieces that you need to be picking out:\n1 \u0026#34;LogRemoteEndpointKafka\u0026#34;: 2 { 3 \u0026lt;snip\u0026gt; 4 \u0026#34;properties\u0026#34;: 5 { 6 \u0026#34;TopicName\u0026#34;: 7 { 8 \u0026#34;type\u0026#34;: \u0026#34;string\u0026#34;, 9 \u0026lt;snip\u0026gt; 10 \u0026#34;default\u0026#34;: \u0026#34;cmdReporter\u0026#34; 11 }, 12 \u0026#34;TLSServerCertificate\u0026#34;: 13 { 14 \u0026#34;type\u0026#34;: \u0026#34;array\u0026#34;, 15 \u0026lt;snip\u0026gt; 16 \u0026#34;type\u0026#34;: \u0026#34;string\u0026#34;, 17 \u0026lt;snip\u0026gt; 18 \u0026#34;placeholder\u0026#34;: \u0026#34;Apple Root CA - G2\u0026#34; 19 } 20 } 21 } 22 }, 23 \u0026#34;TLSClientCertificate\u0026#34;: 24 { 25 \u0026#34;type\u0026#34;: \u0026#34;array\u0026#34;, 26 \u0026lt;snip\u0026gt; 27 \u0026lt;snip\u0026gt; 28 \u0026#34;placeholder\u0026#34;: \u0026#34;server_name.company.com\u0026#34; 29 } 30 } 31 }, 32 \u0026#34;TLSClientPrivateKey\u0026#34;: 33 { 34 \u0026#34;type\u0026#34;: \u0026#34;string\u0026#34;, 35 \u0026lt;snip\u0026gt; 36 \u0026#34;placeholder\u0026#34;: \u0026#34;-----BEGIN CERTIFICATE-----...\u0026#34; 37 } 38 } 39 } 40 } 41 }, The Custom Settings XML output would look similar to:\n1\u0026lt;key\u0026gt;LogRemoteEndpointKafka\u0026lt;/key\u0026gt; 2\u0026lt;dict\u0026gt; 3 \u0026lt;key\u0026gt;TopicName\u0026lt;/key\u0026gt; 4 \u0026lt;string\u0026gt;cmdReporter\u0026lt;/string\u0026gt; 5 \u0026lt;key\u0026gt;TLSServerCertificate\u0026lt;/key\u0026gt; 6 \u0026lt;array\u0026gt; 7 \u0026lt;string\u0026gt;Apple Root CA - G2\u0026lt;/string\u0026gt; 8 \u0026lt;/array\u0026gt; 9 \u0026lt;key\u0026gt;TLSClientCertificate\u0026lt;/key\u0026gt; 10 \u0026lt;array\u0026gt; 11 \u0026lt;string\u0026gt;server_name.company.com\u0026lt;/string\u0026gt; 12 \u0026lt;/array\u0026gt; 13 \u0026lt;key\u0026gt;TLSClientPrivateKey\u0026lt;/key\u0026gt; 14 \u0026lt;string\u0026gt;-----BEGIN CERTIFICATE-----...\u0026lt;/string\u0026gt; 15\u0026lt;/dict\u0026gt; Converting Array Types For example, take the property AuditEventExcludedUsers:\n1\u0026#34;AuditEventExcludedUsers\u0026#34;: 2 { 3 \u0026#34;type\u0026#34;: \u0026#34;array\u0026#34;, 4 \u0026#34;description\u0026#34;: \u0026#34;Users to exclude from audit logging. Recommended: None\u0026#34;, 5 \u0026#34;title\u0026#34;: \u0026#34;AuditEventExcludedUsers\u0026#34;, 6 \u0026#34;options\u0026#34;: 7 { 8 \u0026#34;infoText\u0026#34;: \u0026#34;Key: AllowedEmailDomains\u0026#34; 9 }, 10 \u0026#34;items\u0026#34;: 11 { 12 \u0026#34;type\u0026#34;: \u0026#34;string\u0026#34;, 13 \u0026#34;title\u0026#34;: \u0026#34;Excluded User\u0026#34;, 14 \u0026#34;options\u0026#34;: 15 { 16 \u0026#34;inputAttributes\u0026#34;: 17 { 18 \u0026#34;placeholder\u0026#34;: \u0026#34;dan\u0026#34; 19 } 20 } 21 } 22 }, The resulting custom settings XML would be as follows:\n1 \u0026lt;key\u0026gt;AuditEventExcludedUsers\u0026lt;/key\u0026gt; 2 \u0026lt;array\u0026gt; 3 \u0026lt;string\u0026gt;dan\u0026lt;/string\u0026gt; 4 \u0026lt;/array\u0026gt; Putting It All Together In the interest of time, I don't plan to convert the whole JSON Schema, but you can see how I've come up with a small snippet of the appropriate Custom Settings XML:\n1\u0026lt;dict\u0026gt; 2 \u0026lt;key\u0026gt;AuditLevel\u0026lt;/key\u0026gt; 3 \u0026lt;integer\u0026gt;1\u0026lt;/integer\u0026gt; 4 \u0026lt;key\u0026gt;AuditEventLogVerboseMessages\u0026lt;/key\u0026gt; 5 \u0026lt;false /\u0026gt; 6 \u0026lt;key\u0026gt;AuditEventExcludedUsers\u0026lt;/key\u0026gt; 7 \u0026lt;array\u0026gt; 8 \u0026lt;string\u0026gt;dan\u0026lt;/string\u0026gt; 9 \u0026lt;/array\u0026gt; 10 \u0026lt;key\u0026gt;AuditEventExcludedProcesses\u0026lt;/key\u0026gt; 11 \u0026lt;array\u0026gt; 12 \u0026lt;string\u0026gt;/Applications/Security Tool.app\u0026lt;/string\u0026gt; 13 \u0026lt;/array\u0026gt; 14 \u0026lt;key\u0026gt;LogRemoteEndpointEnabled\u0026lt;/key\u0026gt; 15 \u0026lt;false /\u0026gt; 16 \u0026lt;key\u0026gt;LogRemoteEndpointKafka\u0026lt;/key\u0026gt; 17 \u0026lt;dict\u0026gt; 18 \u0026lt;key\u0026gt;TopicName\u0026lt;/key\u0026gt; 19 \u0026lt;string\u0026gt;cmdReporter\u0026lt;/string\u0026gt; 20 \u0026lt;key\u0026gt;TLSServerCertificate\u0026lt;/key\u0026gt; 21 \u0026lt;array\u0026gt; 22 \u0026lt;string\u0026gt;Apple Root CA - G2\u0026lt;/string\u0026gt; 23 \u0026lt;/array\u0026gt; 24 \u0026lt;key\u0026gt;TLSClientCertificate\u0026lt;/key\u0026gt; 25 \u0026lt;array\u0026gt; 26 \u0026lt;string\u0026gt;server_name.company.com\u0026lt;/string\u0026gt; 27 \u0026lt;/array\u0026gt; 28 \u0026lt;key\u0026gt;TLSClientPrivateKey\u0026lt;/key\u0026gt; 29 \u0026lt;string\u0026gt;-----BEGIN CERTIFICATE-----...\u0026lt;/string\u0026gt; 30 \u0026lt;/dict\u0026gt; 31 \u0026lt;key\u0026gt;AuditEventExcludedUsers\u0026lt;/key\u0026gt; 32 \u0026lt;array\u0026gt; 33 \u0026lt;string\u0026gt;dan\u0026lt;/string\u0026gt; 34 \u0026lt;/array\u0026gt; 35\u0026lt;/dict\u0026gt; Final Thoughts Converting the Schema by hand is tedious and potentially error-prone. Hopefully, converting these JSON Schema documents via automation will soon be made an easy (or not at all required) task.\nAdditional Resources cmdReporter Configuration Profile Schema JSON Schema ","link":"https://blog.terakedis.dev/post/converting-jamf-custom-schema-json-workspace-one-uem/","section":"post","tags":["macOS","Apple","XML"],"title":"Converting Jamf Custom Schema JSON for Workspace ONE UEM"},{"body":"","link":"https://blog.terakedis.dev/tags/xml/","section":"tags","tags":null,"title":"XML"},{"body":"","link":"https://blog.terakedis.dev/tags/actions/","section":"tags","tags":null,"title":"Actions"},{"body":"Short post today - just to cover some thoughts on my most recent asset published to TechZone. I finally sat down and dug out all my notes on troubleshooting macOS and put them all together into a single, comprehensive macOS Troubleshooting Guide. This thing was the result of almost 46 hours of fingers on keys: typing, formatting, and testing. I truly hope you all get some value out of it, and do feel free to send me feedback if you'd like it extended and/or notice something missing.\nWhat did I learn writing this guide While I have been using the Console.app and log command line tool for awhile, this guide provided a great opportunity to really dig into the command line. One of the things I found most interesting was just how powerful the log command can be when you really take the time to understand the predicates. The predicates are what provide the most flexibility with searching and eliminating the noise when hunting the log values. As you start troubleshooting macOS, you'll notice that log filtering via predicates is going to be your quickest win in discovering data about what's happening on the system.\nAnother interesting learning was just how picky macOS can be about apostrophes and quotes. This is particularly problematic if you're copying/pasting from two disparate formats - such as Microsoft Word to Terminal, or Web to Terminal. My best and quickest suggestion - if it doesn't seem to work but you know it should, check for \u0026quot;Straight\u0026quot; quotation marks and not \u0026quot;Curly.\u0026quot;\nThird, there was apparently a huge needa for a document like this. I didn't expect to get so many kudos for something that had been sitting stagnant in my To-Do list for longer than I'd like to admin, but I'm glad it's been well-received!\nFinally, there's so much detail in sysdiagnose files. Generate one and start digging around - you'll be amazed at what you can find in there.\nWhat's the next step With Freestyle orchestrator, scripts, and sensors all announced, you can bet I'll be making some changes as I do some testing. I'll be sure to publish an update when I've managed to get the bulk majority of the new items captured.\nOne thing I didn't really get a chance to dig into yet is how the Developer Debug profiles (downloadable from Apple's developer website) add more into the Unified Logging. As I come across an issue where I have a need to install those, I'll be sure to add any new findings.\nSummary Thanks to the Unified Log that Apple introduced in macOS Sierra, pretty much anything you need to troubleshoot about macOS is going to be contained in the logs. Got a problem with macOS? Check the logging.\n","link":"https://blog.terakedis.dev/post/troubleshooting-macos-management-with-workspace-one/","section":"post","tags":["macOS"],"title":"Troubleshooting macOS Management with Workspace ONE"},{"body":"When I restarted my blogging journey last year, I went with Hugo to generate a static website hosted as a GitHub Pages site. As mentioned, Blogger and WordPress always suffered recurring problems, and maintenance with WordPress still turned into a time suck due to its complexity. By comparison, GitHub has been a nearly painless hosting provider, and the way I've configured it has allowed me to keep drafts hidden by staging in a private repository.\nMy Workflow As I sit down to blog, I start a new post by adding a new markdown page into my posts directory. In the front matter, I use the key-value pair draft: true to notate that the content is still under construction. In other words, my front matter starts to look like the following:\n1--- 2title: \u0026#34;Updated GitHub Actions to Publish Hugo Site From Private to Public Repo.\u0026#34; 3subtitle: \u0026#34;Still keeping half-baked ideas and drafts private!\u0026#34; 4date: 2021-02-13 5draft: true 6tags: [\u0026#34;GitHub\u0026#34;, \u0026#34;Actions\u0026#34;, \u0026#34;Hugo\u0026#34;] 7--- Now, as I get to a good stopping point (either writing in Visual Studio Code for macOS or using Working Copy and iA Writer on my iPad), I can check-in the updated version of that post to my private GitHub repo. My Github Action workflow on the private repository notices when I push code to the main/master. Within the GitHub Action, the Hugo command building the static site ignores drafts, which means the front matter must contain draft: false for Hugo to generate the static webpage (and therefore the content getting copied to the public repository). If content does get developed and copied to the public repository, GitHub Pages (e.g. the live version of my blog) reflects those changes.\nPublishing Hugo Site from a Private Repo to Public GitHub Pages Remember I published the content of my Github Action in GitHub Actions Publish Private Hugo Repo to Public Pages Site. That Action had worked well for me, ... until it didn't. As I attempted to push a new draft recently, the GitHub Actions workflow failed. The specific failure pointed to GitHub deprecating set-env and add-path commands. Ironically, the YAML file contained neither of those two commands! What?! One of the Actions I sourced from the marketplace was misbehaving.\nI set to work updating all the actions and am happy to report the new workflow working as expected.\nAs a reminder, here's how it works:\nThe \u0026quot;name: Checkout\u0026quot; step pulls down the repository to /home/runner/work/\u0026lt;Repo Name\u0026gt;/\u0026lt;Repo Name\u0026gt; on the Ubuntu Runner The \u0026quot;name: Hugo Setup\u0026quot; step downloads and installs the latest non-extended Hugo binary The \u0026quot;name: Run Hugo\u0026quot; step is where Hugo parses the files from my private GitHub repo and builds the static site based the config.toml file (in my case, ./docs). Once parsing completes (and ./docs contains the static site), the workflow uses a Personal Access Token to connect to the Public repository and commit all the new/changed files. Remember, GitHub provides a Free/Personal account up to 2000 minutes of GitHub Actions time per month. In my case, each run of the workflow is taking about 30 seconds. If you find yourself bumping up against this limit, you can quickly reduce your runs by creating a \u0026quot;working\u0026quot; branch to save your work-in-progress. Then you can publish more content at a single time by merging those changes into master and kicking off a new build - things to ponder.\nShow me the Code! Here is my updated main.yml file!\n1# This is a basic workflow to help you get started with Actions 2 3name: Hugo Build \u0026amp; Deploy - Private to Public 4 5# Controls when the action will run. Triggers the workflow on push or pull request 6# events but only for the master branch 7on: 8 push: 9 branches: [ master ] 10 11# A workflow run is made up of one or more jobs that can run sequentially or in parallel 12jobs: 13 # This workflow contains a single job called \u0026#34;build\u0026#34; 14 build: 15 # The type of runner that the job will run on 16 runs-on: ubuntu-latest 17 18 # Steps represent a sequence of tasks that will be executed as part of the job 19 steps: 20 21 # Check Out Repository: https://github.com/marketplace/actions/deploy-to-github-pages 22 - name: Checkout 🛎️ 23 uses: actions/checkout@v2.3.4 24 with: 25 persist-credentials: false 26 27 # Setup Hugo 28 - name: Hugo setup 29 uses: peaceiris/actions-hugo@v2.4.13 30 with: 31 # The Hugo version to download (if necessary) and use. Example: 0.58.2 32 hugo-version: \u0026#39;latest\u0026#39; 33 # Download (if necessary) and use Hugo extended version. Example: true 34 extended: false 35 36 # Runs Hugo to build the Static Site 37 - name: Run Hugo 38 run: | 39 hugo --minify --verbose 40 41 # Deploy the Static Site to Public Repo (GitHub Pages) 42 - name: Deploy 🚀 43 uses: JamesIves/github-pages-deploy-action@4.0.0 44 with: 45 token: ${{ secrets.ACCESS_TOKEN }} 46 repository-name: rterakedis/rterakedis.github.io 47 branch: master # The branch the action should deploy to. 48 folder: docs # The folder the action should deploy. Thoughts? Let me know if you've set up something similar for blogging. Are you using GitHub Actions to automate your publishing workflow? What has been your experience?\n","link":"https://blog.terakedis.dev/post/updated-github-actions-publish-private-hugo-repo-to-public-pages-site/","section":"post","tags":["macOS","GitHub","Actions","automation"],"title":"Updated GitHub Actions to Publish Hugo Site From Private to Public Repo"},{"body":"Some folks recently reached out to me asking for help figuring out how to route NoMAD traffic over VMware Tunnel. Basically, the ask was to use Per-App Tunneling to give NoMAD the ability to obtain Kerberos Tickets and Sync AD passwords without being directly on the Enterprise network. If you're familiar with my previous article about the macOS Catalina Kerberos SSO Plugin, you'll know that Apple's built-in functionality in Catalina doesn't work well with Per-App Tunneling.\nSo Why Try NoMAD If you're not familiar with NoMAD, it's an open-source app meant to help a user on a non-bound Mac obtain Kerberos tickets and sync their local password with a directory (Active Directory) password. It's similar in functionality to Apple's Enterprise Connect tool (available through Apple Professional Services). Our hope with trying NoMAD was to give someone the ability to do the same Kerberos/SSO functionality on Catalina that you can potentially do on Big Sur. The idea was not meant for long-term use and was more-so a stop-gap until devices were upgraded to Big Sur.\nSetting Up NoMAD in Workspace ONE UEM As you go through the setup, you'll need to set up the following:\nCreate and Assign a Custom Settings profile with NoMAD Preferences. Parse the NoMAD.pkg with Workspace ONE Admin Assistant Modify the metadata plist to use an installs array if you would rather check for the app versus the receipt Upload and create the Internal App. Add a post-install script that creates the LaunchAgent. Modify the Tunnel Device Traffic Rules to route traffic to your domain (AD and DNS, or *.yourdomain.com) over the per-app tunnel. SAVE the configuration!! The balance of this blog post hits on the not-so-obvious configurations you'll need to make in order to get this app deployed and working via Workspace ONE:\nProfiles Custom Settings XML for NoMAD Preferences When setting up the Custom Settings XML, you'll need to refer to the list of preferences keys to figure out what is best for your setup. I've included a basic Custom Settings XML profile below. That said, a few gotchas you may want to consider:\nKerberosRealm needs to be specified in ALL CAPS. Determine if you want to supply the LDAPServerList to specify a list of DC's. Without including this, NoMAD attempts to find DC's via DNS. 1\u0026lt;dict\u0026gt; 2 \u0026lt;key\u0026gt;PayloadDisplayName\u0026lt;/key\u0026gt; 3 \u0026lt;string\u0026gt;NoMAD Preferences\u0026lt;/string\u0026gt; 4 \u0026lt;key\u0026gt;PayloadIdentifier\u0026lt;/key\u0026gt; 5 \u0026lt;string\u0026gt;com.trusourcelabs.NoMAD.8B3D35B1-61AD-4017-97BA-A383A61B90FA\u0026lt;/string\u0026gt; 6 \u0026lt;key\u0026gt;PayloadOrganization\u0026lt;/key\u0026gt; 7 \u0026lt;string\u0026gt;VMware\u0026lt;/string\u0026gt; 8 \u0026lt;key\u0026gt;PayloadType\u0026lt;/key\u0026gt; 9 \u0026lt;string\u0026gt;com.trusourcelabs.NoMAD\u0026lt;/string\u0026gt; 10 \u0026lt;key\u0026gt;PayloadUUID\u0026lt;/key\u0026gt; 11 \u0026lt;string\u0026gt;8B3D35B1-61AD-4017-97BA-A383A61B90FA\u0026lt;/string\u0026gt; 12 \u0026lt;key\u0026gt;PayloadVersion\u0026lt;/key\u0026gt; 13 \u0026lt;integer\u0026gt;1\u0026lt;/integer\u0026gt; 14 \u0026lt;key\u0026gt;ADDomain\u0026lt;/key\u0026gt; 15 \u0026lt;string\u0026gt;YOURDOMAIN.COM\u0026lt;/string\u0026gt; 16 \u0026lt;key\u0026gt;KerberosRealm\u0026lt;/key\u0026gt; 17 \u0026lt;string\u0026gt;YOURDOMAIN.COM\u0026lt;/string\u0026gt; 18 \u0026lt;key\u0026gt;RenewTickets\u0026lt;/key\u0026gt; 19 \u0026lt;string\u0026gt;1\u0026lt;/string\u0026gt; 20 \u0026lt;key\u0026gt;SecondsToRenew\u0026lt;/key\u0026gt; 21 \u0026lt;string\u0026gt;7200\u0026lt;/string\u0026gt; 22 \u0026lt;key\u0026gt;ShowHome\u0026lt;/key\u0026gt; 23 \u0026lt;string\u0026gt;0\u0026lt;/string\u0026gt; 24 \u0026lt;key\u0026gt;Template\u0026lt;/key\u0026gt; 25 \u0026lt;string\u0026gt;User Auth\u0026lt;/string\u0026gt; 26 \u0026lt;key\u0026gt;UseKeychain\u0026lt;/key\u0026gt; 27 \u0026lt;true/\u0026gt; 28 \u0026lt;key\u0026gt;Verbose\u0026lt;/key\u0026gt; 29 \u0026lt;true/\u0026gt; 30 \u0026lt;key\u0026gt;DontShowWelcome\u0026lt;/key\u0026gt; 31 \u0026lt;true/\u0026gt; 32 \u0026lt;key\u0026gt;HideAbout\u0026lt;/key\u0026gt; 33 \u0026lt;true/\u0026gt; 34 \u0026lt;key\u0026gt;HidePrefs\u0026lt;/key\u0026gt; 35 \u0026lt;true/\u0026gt; 36 \u0026lt;key\u0026gt;HideGetSoftware\u0026lt;/key\u0026gt; 37 \u0026lt;true/\u0026gt; 38 \u0026lt;key\u0026gt;LDAPServerList\u0026lt;/key\u0026gt; 39 \u0026lt;string\u0026gt;dc1.yourdomain.com,dc2.yourdomain.com\u0026lt;/string\u0026gt; 40 \u0026lt;key\u0026gt;LocalPasswordSync\u0026lt;/key\u0026gt; 41 \u0026lt;true/\u0026gt; 42 \u0026lt;key\u0026gt;RenewTickets\u0026lt;/key\u0026gt; 43 \u0026lt;true/\u0026gt; 44\u0026lt;/dict\u0026gt; Configurations Per-App Tunnel Device Traffic Rules In the Device Traffic Rules, you'll first need to set up the macOS App Definition. The critical info you'll need is this:\nPlatform: macOS Friendly Name: NoMAD Package ID: com.trusourcelabs.NoMAD Designated Requirement: anchor apple generic and identifier \u0026quot;com.trusourcelabs.NoMAD\u0026quot; and (certificate leaf[field.1.2.840.113635.100.6.1.9] /* exists */ or certificate 1[field.1.2.840.113635.100.6.2.6] /* exists */ and certificate leaf[field.1.2.840.113635.100.6.1.13] /* exists */ and certificate leaf[subject.OU] = AAPZK3CB24) With the App definition in place, you can then add your device traffic rule. Set Safari and NoMAD to Tunnel to the list of DCs you've placed in your serverlist (or you can simply with a wildcard) -- *.domain.com. I add Safari so I can do some testing but it is optional.\nScripts PostInstall Script for NoMAD Internal App When setting up NoMAD as an internal application for macOS, you'll want to add the following as a post install script:\n1#!/bin/sh 2/usr/local/bin/hubcli profiles --install 1041 #Where the number is the profile ID for the NoMAD Custom Settings 3/usr/bin/touch /Library/LaunchAgents/com.trusourcelabs.NoMAD.plist 4/bin/cat \u0026gt; /Library/LaunchAgents/com.trusourcelabs.NoMAD.plist \u0026lt;\u0026lt;- EOM 5\u0026lt;?xml version=\u0026#34;1.0\u0026#34; encoding=\u0026#34;UTF-8\u0026#34;?\u0026gt; 6\u0026lt;!DOCTYPE plist PUBLIC \u0026#34;-//Apple//DTD PLIST 1.0//EN\u0026#34; \u0026#34;http://www.apple.com/DTDs/PropertyList-1.0.dtd\u0026#34;\u0026gt; 7\u0026lt;plist version=\u0026#34;1.0\u0026#34;\u0026gt; 8\u0026lt;dict\u0026gt; 9\u0026lt;key\u0026gt;KeepAlive\u0026lt;/key\u0026gt; 10\u0026lt;true/\u0026gt; 11\u0026lt;key\u0026gt;Label\u0026lt;/key\u0026gt; 12\u0026lt;string\u0026gt;com.trusourcelabs.NoMAD\u0026lt;/string\u0026gt; 13\u0026lt;key\u0026gt;LimitLoadToSessionType\u0026lt;/key\u0026gt; 14\u0026lt;array\u0026gt; 15\u0026lt;string\u0026gt;Aqua\u0026lt;/string\u0026gt; 16\u0026lt;/array\u0026gt; 17\u0026lt;key\u0026gt;ProgramArguments\u0026lt;/key\u0026gt; 18\u0026lt;array\u0026gt; 19\u0026lt;string\u0026gt;/Applications/NoMAD.app/Contents/MacOS/NoMAD\u0026lt;/string\u0026gt; 20\u0026lt;/array\u0026gt; 21\u0026lt;key\u0026gt;RunAtLoad\u0026lt;/key\u0026gt; 22\u0026lt;true/\u0026gt; 23\u0026lt;/dict\u0026gt; 24\u0026lt;/plist\u0026gt; 25EOM Summary It's important to note that much of what I've done in this post is applicable to many other Apps that you'd commonly deploy with Workspace ONE UEM. That said, I expect that some of this procedure will change over time as FreeStyle gains additional functionality.\n","link":"https://blog.terakedis.dev/post/deploying-nomad-over-vmware-per-app-tunnel/","section":"post","tags":["NoMAD","macOS","SSO","VPN"],"title":"Deploying NoMAD over VMware Per-App Tunnel"},{"body":"","link":"https://blog.terakedis.dev/tags/nomad/","section":"tags","tags":null,"title":"NoMAD"},{"body":"","link":"https://blog.terakedis.dev/tags/sso/","section":"tags","tags":null,"title":"SSO"},{"body":"","link":"https://blog.terakedis.dev/tags/vpn/","section":"tags","tags":null,"title":"VPN"},{"body":"I'm finding myself using the Terminal quite a bit more in my job. I spent a few minutes over the past few days looking at different ways to make the default terminal layout in macOS better. While there are many plugins out there for doing this (Oh-My-Zsh), I wanted to do something a little more straightforward.\nHow You Change zsh Default Layouts Armin Briegel has a great article about customizing the zsh prompt in his moving to zsh series. The basics come down to the following - make some changes to the file at ~/.zshrc and then enjoy the fruits of your labor! This file is also where you can add zsh functions (e.g. reusable pieces of code.)\nMy Current zsh Defaults Here's the contents of my ~/.zshrc file:\n1PROMPT=\u0026#39;%(?.%F{green}√.%F{red}?%?)%f %B%F{240}%1~%f%b %# \u0026#39; 2 3export PATH=$HOME/.gem/ruby/X.X.0/bin:$PATH 4export GEM_HOME=$HOME/gems 5export PATH=$HOME/gems/bin:$PATH 6 7movc () { 8 ffmpeg -i $1 -c:v libx264 -preset fast \u0026#34;${1%.mov}\u0026#34;-opt.mov 9 newname=\u0026#34;${1%.mov}\u0026#34;-opt.mov 10 origsize=`du -k \u0026#34;$1\u0026#34; | cut -f1` 11 newsize=`du -k $newname | cut -f1` 12 13 echo -e \u0026#39;\\n\u0026#39;$1 $origsize\u0026#34;KB\u0026#34; 14 echo $newname $newsize\u0026#34;KB\u0026#34; 15} 16 17mp4c () { 18 ffmpeg -i $1 -c:v libx264 -preset fast \u0026#34;${1%.mp4}\u0026#34;-opt.mp4 19 newname=\u0026#34;${1%.mp4}\u0026#34;-opt.mp4 20 origsize=`du -k \u0026#34;$1\u0026#34; | cut -f1` 21 newsize=`du -k $newname | cut -f1` 22 23 echo -e \u0026#39;\\n\u0026#39;$1 $origsize\u0026#34;KB\u0026#34; 24 echo $newname $newsize\u0026#34;KB\u0026#34; 25} 26 27mpgc () { 28 ffmpeg -i $1 -c:v libx264 -preset fast \u0026#34;${1%.mpg}\u0026#34;-opt.mpg 29 newname=\u0026#34;${1%.mpg}\u0026#34;-opt.mpg 30 origsize=`du -k \u0026#34;$1\u0026#34; | cut -f1` 31 newsize=`du -k $newname | cut -f1` 32 33 echo -e \u0026#39;\\n\u0026#39;$1 $origsize\u0026#34;KB\u0026#34; 34 echo $newname $newsize\u0026#34;KB\u0026#34; 35} 36 37# Move \u0026#39;up\u0026#39; so many directories instead of using several cd ../../, etc. 38up() { 39 cd $(eval printf \u0026#39;../\u0026#39;%.0s {1..$1}) \u0026amp;\u0026amp; pwd; 40} Breaking Down the Defaults The first line (PROMPT='%(?.%F{green}√.%F{red}?%?)%f %B%F{240}%1~%f%b %# ') was pulled straight from Armin's Customizing the zsh prompt article. I love it because it's generally very minimal, but when you start diving into the filesystem it starts to give you an idea of where you're at. He's also embedded a return-code indicator, which is handy when running long commands.\nThe next three export lines are probably recognizable by anyone building Jekyll websites.\nThe next 3 sections of code are functions. In my job, I create a number of video files that get shared via Powerpoints and/or posted to YouTube. These 3 functions are shortcut command lines to use FFmpeg and drastically reduce the file size on video content. Basically, I can take an MP4 file output from Camtasia, and run mp4c video.mp4 to kick off an FFmpeg compression that outputs that same file (reduced in size) as video-opt.mp4. Credit to jprichards for the code for this one.\nThe last function I found on Reddit and find myself doing this exact thing constantly. It's going to be handy being able to simply type up 3 to change directories.\n","link":"https://blog.terakedis.dev/post/modify-zshell-defaults/","section":"post","tags":["macOS","Apple","Terminal"],"title":"Modify ZShell Defaults in macOS"},{"body":"","link":"https://blog.terakedis.dev/tags/terminal/","section":"tags","tags":null,"title":"Terminal"},{"body":"If you've read my blogs about macOS Catalina Kerberos SSO over Per-App Tunnel and the followup, you'll know that this has been a use-case I'm interested in solving. I put a great deal of effort into filing feedback with Apple and providing steps to replicate the issue. I was quite excited when I saw the per-app Tunnel improvements specifically mentioned in the WWDC videos, and hoped perhaps some changes were made to enable this functionality.\nRemembering the Use-Cases I sat down and re-hashed all the old use-cases I drew up for making this work:\nWhat if the new Kerberos SSO Extension in macOS BigSur was just another one of those applications that you redirected over a Tunnel? Could you, in theory, get Kerberos Tickets to an unbound (non-AD joined) Mac to use for authenticating internal websites over Per-App Tunnel? Could you use the Kerberos SSO Extension to sync your local (non-mobile) macOS User's password with the on-prem AD password over Per-App Tunnel? Could you change your on-prem AD password remotely over Per-App Tunnel when it neared the expiration date? Beta Testing Results I ran through the same validation tests again and here's what happened:\nTesting Item Per-App Tunnel Kerberos Ticket Obtained over Tunnel Yes! Password Expiration Date Correct Yes! Extension Detects local PW different from AD Yes! User Change AD Password via Extension over Tunnel Yes! Applicability to Workspace ONE Internally, we had some discussion on why to use the SSO Extension instead of Cert-Based Auth with Workspace ONE Access (and Device Compliance). Honestly, I think these two solutions target drastically different use cases. First, the SSO Extension directly addresses password sync between a local (to macOS) user account and identity in Active Directory. Second, I see the SSO Extension as an easy way to enable SSO for internal web apps (such as those hosted on a Windows server in IIS). Since the authentication can be done by IIS, you can provide a single sign-on experience to those apps without needing to build-in SAML support (and subsequently Workspace ONE Access integration). With macOS leveraging the Unified Access Gateway for per-app tunnel, you can leverage compliance in Workspace ONE to remove the per-app VPN profile.\nNext Steps I'll be continuing to test this and working with our internal Tunnel team. In the mean time, I've written up the steps in Deploying Workspace ONE Tunnel for macOS [TechZone]. I invite anyone beta testing Big Sur to try it out and send me feedback as to your experience!\n","link":"https://blog.terakedis.dev/post/macos-bigsur-kerberos-sso-over-vpn/","section":"post","tags":["macOS","SSO","VPN"],"title":"macOS Big Sur and Kerberos SSO via Per-App Tunnel"},{"body":"I restarted my blogging journey earlier this year when I started looking into Jekyll Hugo to generate a static website. I had past experience with Blogger and Wordpress, but frankly had periodic problems with both platforms that ended up being a time suck. As it has been, Hugo has been a simplistic publishing method and GitHub a reliable (and FREE) hosting provider. Yet, my desire to keep my drafts private (.e.g the use of 2 separate repositories) has created a small overhead in that I have to build and manually commit the website changes to the public repository to make them live.\nGithub Actions GitHub Actions is a well-documented feature... that takes a little trial-and-error for us amateur devs (e.g. scripting sysadmins). When I first started learning, I really couldn't make heads-or-tails of it. But really, all you need to do is think of it as an automation workflow, triggered by GitHub events (check-in, etc). This automation engine spins up a temporary virtual machine, runs whatever code you need it to do, and then destroys the VM.\nWhat's the Gotcha? I found a bunch of articles where folks were using GitHub actions to deploy their Hugo sites based on repo check-ins. Cool, right? Push new code, and a few minutes later you have a new site live on the Internet. Except... these folks are using a single public repository for their GitHub pages and basically bouncing code between branches or folders in a single branch. I couldn't find anyone doing the same thing as me -- publishing the Hugo \u0026quot;Source\u0026quot; to a private repository, and then publishing just the static site to the Public GitHub Pages repository. I set out to figure this out and save myself some time.\nPublishing Hugo Site from a Private Repo to Public GitHub Pages I managed to cobble together a workflow using pieces from a couple different actions in the GitHub Actions marketplace. A few things to keep in mind:\nThe \u0026quot;name: Checkout\u0026quot; step in the job pulls down the repository to /home/runner/work/\u0026lt;Repo Name\u0026gt;/\u0026lt;Repo Name\u0026gt; on the Ubuntu Runner. The \u0026quot;name: Hugo Setup\u0026quot; step downloads and installs the actual hugo binary. You can see I used the \u0026quot;latest\u0026quot; version and skipped the extended version. When the name: Run Hugo\u0026quot; step happens, this is where Hugo is actually reading the content of the files downloaded from my private GitHub repo, and building the static site in the subdirectory specified in the config.toml file (in my case, ./docs). Once the site finished building, Github uses a Personal Access Token to connect to the Public repository (e.g. NOT the repository running the workflow) and commit all the new/changed files. The beauty of this whole setup is that GitHub provides a Free/Personal account up to 2000 minutes of GitHub Actions time per month. For a personal website, this should be more than enough. Also, if you find yourself bumping up against this limit, you can easily reduce your runs by creating a \u0026quot;working\u0026quot; branch to save your work-in-progress. Then you can publish more content at a single time by merging those changes into master and kicking off a new build. Things to think about.\nShow me the Code! Here you go!\n1# This is a basic workflow to help you get started with Actions 2 3name: Hugo Build \u0026amp; Deploy - Private to Public 4 5# Controls when the action will run. Triggers the workflow on push or pull request 6# events but only for the master branch 7on: 8 push: 9 branches: [ master ] 10 11# A workflow run is made up of one or more jobs that can run sequentially or in parallel 12jobs: 13 # This workflow contains a single job called \u0026#34;build\u0026#34; 14 build: 15 # The type of runner that the job will run on 16 runs-on: ubuntu-latest 17 18 # Steps represent a sequence of tasks that will be executed as part of the job 19 steps: 20 21 # Check Out Repository: Based on https://github.com/marketplace/actions/deploy-to-github-pages 22 - name: Checkout 🛎️ 23 uses: actions/checkout@v4 24 with: 25 persist-credentials: false 26 submodules: true 27 28 # Setup Hugo in the Ubuntu Runner 29 - name: Hugo setup 30 uses: peaceiris/actions-hugo@v2.6.0 31 with: 32 # The Hugo version to download (if necessary) and use. Example: 0.58.2 33 hugo-version: \u0026#39;latest\u0026#39; 34 # Download (if necessary) and use Hugo extended version. Example: true 35 extended: true 36 37 # Runs Hugo to build the Static Site 38 - name: Run Hugo 39 run: | 40 hugo --minify --verbose 41 42# Deploy the Static Site to Public Repo (GitHub Pages) 43 - name: Deploy 🚀 44 uses: JamesIves/github-pages-deploy-action@v4 45 with: 46 ssh-key: ${{ secrets.DEPLOY_KEY }} 47 repository-name: rterakedis/rterakedis.github.io 48 branch: master # The branch the action should deploy to. 49 folder: docs # The folder the action should deploy. What about secrets.access_token? So you noticed that did you? Yes, this is basically a way to allow the runner/action to authenticate to a different repository. If you read the documentation, it turns out the GitHub action executes in the permission/scope of the resposity to which it s permitted to do so. In my specific use-case, the runner needs to commit and push files to an entirely separate GitHub repository. By setting up the Personal Access Token (PAT) in my Account's Developer Settings, I'm able to add that PAT in the repository's \u0026quot;secrets\u0026quot; stash so that the action can authenticate to the Public repository.\nLessons Learned In my site, I wanted to make sure that any files I changed were sure to get published. As such, I cleared out the /docs folder in my private repository so that each time hugo runs on the runner that folder contains all new files. This prevents a situation whereby hugo doesn't replace files that have already been generated. It also means I don't have to do a heavy-handed wipe on the folder downloaded to the runner before running the hugo build. All-in-all, this just seems cleaner.\nI also modified the .gitignore file at the root of my project. I did this so that I could continually test locally (using hugo server --buildDrafts) without having to worry about git trying to track the static site files used by the hugo server. Here's what my .gitignore file contains:\n1.DS_Store 2public 3docs Another lesson learned is that YAML is VERY particular about indentation and spacing. As this was my first experience with YAML, I went through a few bombed GitHub Actions deployments until I figured out that one of the lines in my yaml file was not spaced correctly.\nThoughts? This is my first foray into Github Actions, and it works pretty reliably. I've also found that using the GitHub Actions I'll now be able to blog on my iPad using Working Copy and iA Writer (in addition to Visual Studios Code on my Mac).\n","link":"https://blog.terakedis.dev/post/github-actions-publish-private-hugo-repo-to-public-pages-site/","section":"post","tags":["GitHub","Actions","automation"],"title":"Using GitHub Actions to Publish Hugo Site From Private to Public Repo"},{"body":"I've been going back and forth with Apple about some of the issues I previously found using the macOS Catalina Kerberos SSO over Per-App VPN. As it turns out, they acknowledged some of the issues I was seeing and are supposedly working on a fix. I've been watching the past few beta releases for macOS, and I've not yet seen anything in the release notes to indicate they've implemented any fixes.\nPartial Workaround In the meantime, they did offer up a partial workaround. It would seem some of the functionality in the Kerberos SSO extension in macOS Catalina is based on the MIT library built-in to macOS. If you remember from the last post, one of the problems that I saw was an inability to change the user's AD password over per-App VPN. Doing so gave me a warning “Password Change Failed. Configuration file does not specify default realm”. This didn't really make sense as you're required to specify the realm in the Kerberos SSO extension MDM payload, and I could confirm that the parameter was being included in the XML for the payload.\nApple offered up that to get around that issue, we had to create an /etc/krb5.conf file and add the following two lines in it:\n1[libdefaults] 2 default_realm = EXAMPLE.ORG I was also instructed to run pkill -9 KerberosExtension to kill the Kerberos Extension to ensure it reloaded the configuration and read the krb5.conf file.\nResults Post-Workaround I ran through the same validation tests again and here's what happened:\nTesting Item Per-App VPN (Tunnel) GlobalProtect (VPN) Kerberos Ticket Obtained over VPN Yes! Yes! Password Expiration Date Correct No! No! Extension Detects local PW different from AD No! Yes! User Change AD Password via Extension over VPN Yes! Yes! Apple also instructed me to continue testing and capture logs using the following procedure:\nEnable the necessary debug modes by running: 1$ sudo log config --mode \u0026#34;level:debug,persist:debug\u0026#34; --subsystem com.apple.AppSSO 2$ sudo log config --mode \u0026#34;level:debug,persist:debug\u0026#34; --subsystem com.apple.Heimdal 3$ sudo log config --mode \u0026#34;level:debug,persist:debug\u0026#34; --subsystem org.h5l.gss run pkill -9 KerberosExtension AppSSOAgent KerberosMenuExtra 1$ pkill -9 KerberosExtension AppSSOAgent KerberosMenuExtra Reproduce the issue. Gather and send us a sysdiagnose (sudo sysdiagnose). Please also note the timestamps. To reset the logs run: 1$ sudo log config --subsystem com.apple.AppSSO --reset 2$ sudo log config --subsystem com.apple.Heimdal --reset 3$ sudo log config --subsystem org.h5l.gss --reset Next Steps I'll be continuing to test this and working with our internal Tunnel team. I'm hoping to get Apple additional information so they can see what is causing the remaining issues. I'm excited to get this working and having the ability to leverage this Extension while off-network. Feel free to leave comments below!\n","link":"https://blog.terakedis.dev/post/macos-catalina-kerberos-sso-over-vpn-followup/","section":"post","tags":["macOS","SSO","VPN"],"title":"macOS Catalina Kerberos SSO over VPN Followup"},{"body":"When Big Sur released, I noticed an issue trying to discover what macOS versions were available in SoftwareUpdate. I worked around this by using the installinstallmacos.py script to download full installers from the store. This script/tool is solid, but I was really hoping to be able to just do the updates using softwareupdate. Awhile back I filed feedback with Apple to the following:\nThe --fetch-full-installer parameter for the softwareupdate command line tool is awesome, but there's currently no way to discover the list of available values for the --full-installer-version parameter. Can you please add a --list-installer-versions parameter (or something to that effect) that would show the list of available full installer versions that softwareupdate can download? My understanding is the installer versions relate to the product marketing version (like 11.0, or 10.15.7), but as those versions are added/removed it would be nice easily discover that from the command line.\nI admittedly forgot about the feedback after filing it (hoping that someday someone might see it and care).\nNew Functionality Arrives To my surprise, I saw a note replying in December saying they fixed this in a new release. The response was vague and pointed to the \u0026quot;About this mac\u0026quot; functionality, which seemed odd. Nevertheless, I went into the command line and started poking around. I ran the softwareupdate command with the help parameter and found a new option:\n1softwareupdate --help 2usage: softwareupdate \u0026lt;cmd\u0026gt; [\u0026lt;args\u0026gt; ...] 3 4** Manage Updates: 5\t-l | --list\tList all appropriate update labels (options: --no-scan, --product-types) 6\t-d | --download\tDownload Only 7\t-i | --install\tInstall 8\t\u0026lt;label\u0026gt; ...\tspecific updates 9\t-a | --all\tAll appropriate updates 10\t-R | --restart\tAutomatically restart (or shut down) if required to complete installation. 11\t-r | --recommended\tOnly recommended updates 12\t--list-full-installers\tList the available macOS Installers 13\t--fetch-full-installer\tInstall the latest recommended macOS Installer 14\t--full-installer-version\tThe version of macOS to install. Ex: --full-installer-version 10.15 15\t--install-rosetta\tInstall Rosetta 2 16\t--background\tTrigger a background scan and update operation 17 18** Other Tools: 19\t--dump-state\tLog the internal state of the SU daemon to /var/log/install.log 20\t--evaluate-products\tEvaluate a list of product keys specified by the --products option 21\t--history\tShow the install history. By default, only displays updates installed by softwareupdate. 22\t--all Include all processes in history (including App installs) 23 24** Options: 25\t--no-scan\tDo not scan when listing or installing updates (use available updates previously scanned) 26\t--product-types \u0026lt;type\u0026gt;\tLimit a scan to a particular product type only - ignoring all others 27\tEx: --product-types macOS || --product-types macOS,Safari 28\t--products\tA comma-separated (no spaces) list of product keys to operate on. 29\t--force\tForce an operation to complete. Use with --background to trigger a background scan regardless of \u0026#34;Automatically check\u0026#34; pref 30\t--agree-to-license\tAgree to the software license agreement without user interaction. 31 32\t--verbose\tEnable verbose output 33\t--help\tPrint this help Did you catch that?\n--list-full-installers\tList the available macOS Installers\nYES! We can now get the list of installers that are available to download!\n1softwareupdate --list-full-installers 2Finding available software 3Software Update found the following full installers: 4* Title: macOS Big Sur, Version: 11.2.3, Size: 12211077798K 5* Title: macOS Big Sur, Version: 11.2.2, Size: 12200254955K 6* Title: macOS Big Sur, Version: 11.2.1, Size: 12199403070K 7* Title: macOS Catalina, Version: 10.15.7, Size: 8248985973K 8* Title: macOS Catalina, Version: 10.15.7, Size: 8248854894K 9* Title: macOS Catalina, Version: 10.15.6, Size: 8248781171K 10* Title: macOS Mojave, Version: 10.14.6, Size: 6038419486K 11* Title: macOS High Sierra, Version: 10.13.6, Size: 5221689433K Happy Downloading and a huge Thank You to the folks at Apple!\n","link":"https://blog.terakedis.dev/post/new-key-added-softwareupdate-command-macos/","section":"post","tags":["macOS","Terminal"],"title":"New Key Added to SoftwareUpdate Command in macOS"},{"body":"When I restarted my blogging journey in 2020, I switched from Jekyll to Hugo hosted in GitHub pages. It's been a relatively painless journey, and kudos to GitHub as a rock-solid hosting provider. I've covered it before (Initial Setup and First Update), but I've been incredibly happy with private-to-public publishing workflow that allows me to keep drafts and work-in-progress hidden. That said, a recent comment gave me reason to make another update to the Workflow. Read on for more detail...\nUpdating Security for Hugo Private-to-Public Publishing Workflow I initially set up my publishing workflow to use a Personal Access Token to deploy the Hugo site to the public GitHub pages repository. To do this, I configured the Personal Access Token and then saved it as a Secret. You can see this being leveraged in the Action's main.yml file in the token field (${{ secrets.ACCESS_TOKEN }}):\n1 # Deploy the Static Site to Public Repo (GitHub Pages) 2 - name: Deploy 🚀 3 uses: JamesIves/github-pages-deploy-action@4.0.0 4 with: 5 token: ${{ secrets.ACCESS_TOKEN }} 6 repository-name: rterakedis/rterakedis.github.io 7 branch: master # The branch the action should deploy to. 8 folder: docs # The folder the action should deploy. A recent comment (Thanks Stefan!) on my (Initial Setup brought to my attention a better way to handle security in the publishing workflow. The commenter made the suggestion to leverage Deploy Keys, rather than Personal Access Tokens. By making this change, the Deploy Key access rights are limited to the repository where I need that key for publishing. Contrast this to the access rights granted by a personal access token, which basically gives access to any of the repos within my GitHub account. The deploy key makes far more sense in limiting scope of access in the event the secret is leaked or discovered.\nAdding a Deploy Key to the Public Repository A Deploy Key is an SSH key set you configure in your repository to grant client read-only (or read-write) access. Before making any changes to GitHub Actions, I had to configure the new Deploy Key for the repository containing the GitHub Pages site (i.e. the Public-facing Repository). Here's how to do it:\nOn your computer, generate a passwordless SSH key (example: ssh-keygen -f id_gha -t rsa -m pem -b 4096 -C \u0026quot;youremailhere@example.com\u0026quot; -N \u0026quot;\u0026quot;) NOTE The command generates both your private and public keys (id_gha and id_gha.pub). Be sure to generate them in a directory where you want them saved/secured. You do not want your working directory to be the local working directory for your GitHub repo, as you could inadvertently publish them to GitHub! Browse to the Public Repository where GitHub Pages is hosted. Click on the Settings tab for the repository NOTE If your browser is windowed, it may be hidden in the elipsis ... to the right) Click on Deploy Keys Click on Add Deploy Key Enter a title for the key (i.e. GH-Actions-Workflow), paste the contents of the public key file (starts with ssh-rsa), and click the checkbox for Allow Write Access Click the Add Key button to save the public key to your repository Changing the GitHub Actions Workflow to Use the Deploy Key With the Deploy Key added to the Public Repo hosting my GitHub pages website, I now need to change the GitHub Actions workflow for my private repository. Since I was previously using a Personal Access Token, I need to make changes to both the Secrets AND the workflow to start using the Deploy Key. The detail on how to use the Deploy Key for the Deploy step in my workflow came from JamesIves' documentation. Here's what I did:\nChanges to the Private Repository First, I needed to make some changes within the settings for the public repo where I host my blog.\nBrowse to the Private Repository where your Hugo site's source is committed. Click on the Settings tab for the repository NOTE If your browser is windowed, it may be hidden in the elipsis ... to the right) Click on Secrets Click on New Repository Secret Enter a name (i.e. DEPLOY_KEY) and paste the contents of the private key file (starts with -----BEGIN) NOTE Be sure to paste the entire contents of the file, including the -----BEGIN RSA PRIVATE KEY----- and -----END RSA PRIVATE KEY----- lines. Click on Add Secret Changes in Code Once the changes to the private repository settings were complete, I had to then make some changes to the YAML file that details my GitHub Actions workflow. Here's how:\nOpen the main.yml file in the /.github/workflows folder in my private repository code. Find the step starting with name: Deploy 🚀 Changed the line from token: ${{ secrets.ACCESS_TOKEN }} to ssh-key: ${{ secrets.DEPLOY_KEY }} Save the file and commit the YAML file to GitHub NOTE: I also took the opportunity to change the revision of the Deploy Step's uses: line to take advantage of the latest code. See below...\nShow me the Code! Here is my entire newly updated main.yml file!\n1# This is a basic workflow to help you get started with Actions 2 3name: Hugo Build \u0026amp; Deploy - Private to Public 4 5# Controls when the action will run. Triggers the workflow on push or pull request 6# events but only for the master branch 7on: 8 push: 9 branches: [ main ] 10 11# A workflow run is made up of one or more jobs that can run sequentially or in parallel 12jobs: 13 # This workflow contains a single job called \u0026#34;build\u0026#34; 14 build: 15 # The type of runner that the job will run on 16 runs-on: ubuntu-latest 17 18 # Steps represent a sequence of tasks that will be executed as part of the job 19 steps: 20 21 # Check Out Repository: https://github.com/marketplace/actions/checkout 22 - name: Checkout 🛎️ 23 uses: actions/checkout@v2.4.0 24 with: 25 persist-credentials: false 26 submodules: true # I recently changed themes and started including the theme as a submodule 27 28 29 # Setup Hugo 30 - name: Hugo setup 31 uses: peaceiris/actions-hugo@v2.5.0 32 with: 33 # The Hugo version to download (if necessary) and use. Example: 0.58.2 34 hugo-version: \u0026#39;latest\u0026#39; 35 # Download (if necessary) and use Hugo extended version. Example: true 36 extended: true # I recently changed themes and the new theme needs the SCSS support in hugo extended version 37 38 # Runs Hugo to build the Static Site 39 - name: Run Hugo 40 run: | 41 hugo --minify --verbose 42 # Deploy the Static Site to Public Repo (GitHub Pages): https://github.com/marketplace/actions/deploy-to-github-pages 43 - name: Deploy 🚀 44 uses: JamesIves/github-pages-deploy-action@4.1.7 45 with: 46 ssh-key: ${{ secrets.DEPLOY_KEY }} 47 repository-name: rterakedis/rterakedis.github.io 48 branch: master # The branch the action should deploy to. 49 folder: docs # The folder the action should deploy. Clean Up the Personal Access Token Since I wasn't using the Personal Access Token for anything else in GitHub, I decided to do some cleanup. I highly recommend this as the token can grant more access than you'd want if the token/secret was ever leaked or stolen. Here's how I went about cleaning up:\nRemove the Secret Browse to the Public Repository where GitHub Pages is hosted. Click on the Settings tab for the repository NOTE If your browser is windowed, it may be hidden in the elipsis ... to the right) Click on Secrets Click REMOVE next to the Personal Access Token Secret (i.e. ACCESS_TOKEN), and then click Yes, Remove this Token Remove the Token Click on your account in the right corner and click on Settings to go to your Account Settings Do not confuse Account Settings with Repo Settings. The Account Settings screen actually says \u0026quot;Account Settings\u0026quot; in the top of the left-side menu Click on Developer Settings Click on Personal Access Tokens Click Delete next to your GH-Hugo-Workflow token Click I Understand, delete this token All cleaned up!\nThoughts? Let me know if you've set up something similar for blogging. Are you using GitHub Actions to automate your publishing workflow? What has been your experience?\n","link":"https://blog.terakedis.dev/post/another-github-actions-update-change-hugo-publish-deploy-keys/","section":"post","tags":["GitHub","Actions","automation"],"title":"Another GitHub Actions Update - Using Deploy Keys Instead Of Personal Access Tokens"},{"body":"","link":"https://blog.terakedis.dev/tags/digibujo/","section":"tags","tags":null,"title":"DigiBujo"},{"body":"Last year, my wife introduced me to an entirely foreign-to-me concept of journaling known as Bullet Journaling. For those of you uninitiated, think of bullet journaling as an extensible planner with a heavy focus on the index to help bring order to chaos. A bullet journal is flexible, allowing you to organize tasks, events, collections (groups of tasks, ideas, and more). Bullet Journaling was born of a need to bring all the ideas/tasks/events plaguing us in the digital age and put them to paper in a single place. By doing so, there was less chance of forgetting things, and the repetition of writing helped bring mindfulness to the list.\nBullet Journaling Basics A bullet journal consists of an Index, Future Logs, Monthly Logs, Daily Logs, and Collections. As you start a Bullet Journal, you first set up the Future Logs and add their page numbers to the Index. Next, you start the Monthly Log to capture all the meaningful things you need to do that month and the time you have to accomplish them. Finally, you start your Daily logs, which is the list of entries (tasks, events, and notes) you curate day-to-day via rapid logging (short bulleted sentences).\nThe beauty of the bullet journal is that each time you review (adding, completing, or moving) items, you bring mindfulness of your tasks at hand and iterate on the need to keep the remaining entries. Iterating allows you to decide if entries need moved (perhaps to a new month) or deleted as no longer worth your time. You notate what happens with each entry by using the bullets (or some signifying markup). You can find a much better explanation of it in the \u0026quot;How to Bullet Journal\u0026quot; video.\nWhy the Digital Bullet Journal on iPad Most folks (from what I can tell) advocate pen-to-paper for a Bullet Journal. I'm a techie, and the idea of pen-to-paper is the equivalent of a real-life PC Load Letter error for me. I liked the system, and what it meant, but I also knew that much of what I read and watched could be applied digitally with the right tools and app. I decided I wanted to give Bullet Journaling a try, but more specifically wanted to try a Digital Bullet Journal (DigiBuJo). I did some testing and research and found some folks talking about the capabilities of GoodNotes (now version 5). Specifically, I've found GoodNotes quite handy for the following reasons:\nPinch to Zoom: I can zoom in and focus on the part of a page when I'm migrating or drawing. Templating Bullet Journal: I keep an extra Digital Bullet Journal (in other words, a GoodNotes \u0026quot;Notebook\u0026quot;) for creating new templates to export. If you draw a page layout in your Digital Bullet Journal and then start using it directly, you can damage the layout with the Erase tool. By drawing the layout and exporting it as a template, GoodNotes treats the imported template like a \u0026quot;background,\u0026quot; which can't be damaged by the Erase tool. The Lasso Tool: I use the lasso tool to select entries and copy/paste them when migrating. This is one of the significant parts about a Digital Bullet Journal -- you can save a bit of time during migration because there's limited writing. I haven't noticed any negative impact on mindfulness of the items, because I'm still reading them and figuring out where to Migrate them. The Erase tool: SO much cleaner than white-out, or erasing pencil marks. Also, if you pinch-out to zoom in, you can get accurate erasing if you're drawing/sketching. Excellent 2nd-Gen Apple Pencil Support: It just works, and the accuracy of the 2nd Gen Apple Pencil on the iPad Pro is paper-like. GoodNotes Search Function: GoodNotes has a Fantastic search function that even searches your handwritten notes! Yes, you read that correctly - searching handwritten notes. If I didn't know better, I would wager the GoodNotes team supplies CoreML models for handwriting recognition or something. It's incredible to me how well it works. iCloud Sync: I leverage this to get my notes on my Phone (for on-the-go reference), and also to sync to macOS (I park it in the corner while I work). My Daily Log is always in front of my face or just a few clicks away. How I Set Up My Digital Bullet Journal on iPad I started setting up a bullet journal in 2019. I created the following basic templates that I saved in iCloud and imported as I built out my bullet journal:\n2-Page Index 2-Page Future Log 2-Page Future Log Jan-Jun 2-Page Future Log Jul-Dec 2-Page Monthly Log with Tasks 2-Page Blank Project Template 2-Page Blank Header Template 8.5x11 Dot Grid 8.5x11 Dot Grid Divided 2-Pages My goal with the templates was to set up a functional journal and not have it be eye-catching. If you search YouTube, you can find several folks making highly creative layouts for their Bullet Journals. That's simply not something I was willing to spend time on.\nKey I wanted to manage BOTH my work and personal lives in a single journal, so I modified the Bullet Journal's standard key to a hybrid of some other keys I've seen folks using:\nThe new key gave me the ability to differentiate between work (square) tasks or deliverables, personal (circle) tasks, and events (triangle, which could be both personal or work). I also used the same set of markup consistently across those base shapes as I iterated over the list (arrows, strikethrough, asterisk, and fill).\nFuture Logs The first two pages of my Digital Bullet Journal are the two Future Log pages (2-Page Future Log Jan-Jun and 2-Page Future Log Jul-Dec. In the future logs, I track recurring events like birthdays and anniversaries and any other notable events planned farther than a month out (family vacations, work trips, and more). I refer to this every time I create a new Monthly Log.\nMonthly Logs Rather than create a Monthly Log for all 12 months at once, I typically create just the first 3 months (January, February, March) at the beginning. As each month passes, I create the next monthly log for 3 months out. In other words, at the end of January, I create the April monthly log. I find this saves me from having to bore myself with slogging through all 12 monthly logs upfront (but to each their own).\nI start each Monthly log using the 2-Page Monthly Log with Tasks template. I fill in the day's number and abbreviation (S, M, T, and more), and then bring over the items from my Future Log into the calendar (left) side. I then start adding to my list of monthly tasks I'd like to get done (work and personal) on the right. I migrate tasks into this month from the previous month as I iterate based on whether they're still valuable. However, I place some tasks into each month based on my delivery planning for work. Remember, my tasks list contains both work and personal tasks (noted by their different key icons - circle versus square). As such, some tasks that I feel are relevant don't necessarily get moved to the next month. Instead, I sometimes move a task two or three months out based on its priority. It subsequently gets re-prioritized as I perform migration at the beginning of that month.\nDaily Logs I use the 8.5x11 Dot Grid Divided 2-Pages template for my Daily Logs. I draw a rectangle to highlight the date and then list my work and personal tasks under that date header. Periodically throughout the day, I check that daily log to make sure I remain focused on the tasks at hand and determine if a task needs canceled or migrated (either short term (next monthly log) or held (to Future Log)).\nProjects or Collections I use the 2-Page Blank Project Template to store collections. A few things I've done as collections include:\nMeal Plans (hand-drawn grids with menu ideas) Date Night Ideas (restaurants and activities - did we like them?) Gift Idea Lists (rather than scrambling 2 weeks before Christmas/Birthdays) The list is endless, and these collections help you get things out of your head and onto (virtual) paper.\nMy Modifications to the System As I've been using the system, I've made a few modifications to how I use the system:\nNo Index Page: I leverage the Outline functionality in GoodNotes, and create an outline entry for each Future Logs, Monthly Logs, Collections, and then the page containing the current day's Daily Log. Here's a view of the start to my 2020 Bullet Journal: Modify the Outline as Necessary: When I start a new page for Daily Logs, I move the \u0026quot;TODAY\u0026quot; outline entry to the new page. I do the same for \u0026quot;THIS MONTH.\u0026quot; Less Reliance on Future Log: I rarely need to migrate an item more than 3 months out, so I skip the future log and move the item directly to the month I plan to tackle it. On the off-chance I were to complete ALL my monthly tasks (hasn't happened yet), I would simply migrate tasks back in from the next month. Daily Logs Span Multiple Days: I generally have two or more work items in my daily log that I can't complete in a single day. Rather than re-writing these items over and over (and over) again in Daily Log entries, I keep the same daily log entry open and continuously add my \u0026quot;quick hit\u0026quot; items into that single log entry. As such, I only close that Daily Log entry when I complete the long-running Work-related entries, or the work priorities change. At that point, I include the finished date (such as \u0026quot;2/10 --\u0026gt; 2/17\u0026quot;) on the entry list, and then migrate as necessary to a new list (\u0026quot;2/18 --\u0026gt; \u0026quot;). Opportunities for Improvement I'm always looking at ways others are using their Bullet Journals. A few of the ideas I've liked that I've thought about integrating also:\nColumnar Monthly Log Per Family Member: I expect as my children get busier with extracurriculars, it may be handy to have this at-a-glance view of who needs to go where each day. Calendex: Not sure if it would work for me, but it looks cool. Habit and Training Trackers: I'd like to set up a good layout for tracking some self-improvement (personal and professional) items. Color!: If I had extra time, I'd love to lay out some slightly more artistic templates. One can dream... Feedback I'd love to hear from anyone that finds this helpful! Feel free to reach out and let me know what you've done from a Digital Bullet Journal perspective.\n","link":"https://blog.terakedis.dev/2020-03-06-organizing-adapted-digital-bullet-journal-ipad-goodnotes/","section":"post","tags":["DigiBujo"],"title":"My Adapted Digital Bullet Journal via iPad Pro and GoodNotes 5"},{"body":"I had a few folks recently approach me on the MacAdmins slack asking for help with deploying Microsoft Defender ATP for Mac. We got it working, but it came down to 2 issues: conflicting documentation and Jamf/inTune specific templates. Once I was able to parse through the Jamf/InTune documentation, we were able to put together some guidance. We recently published this guide to the EUC Samples GitHub Repository.\nConflicting Documentation The initial problem I found was some confusion generated by Microsoft's documentation. The first article, Set Preferences for Microsoft Defender ATP referenced a payloadType of com.microsoft.wdav. These preferences seem related to the UI and various configuration settings. In other words, it controls what changes can be made in the UI if opened by the end-user.\nThe second article, JAMF-based deployment for Microsoft Defender ATP for Mac, referenced a payloadType of com.microsoft.wdav.atp. This \u0026quot;MDM Deployment\u0026quot; specific article only mentions the profile downloaded as part of the Onboarding package. This specific profile looks geared towards licensing and linking the Defender ATP to your specific tenant in the threat protection cloud for telemetry.\nFor anyone uninitiated, the payloadType in a configuration profile is essentially the key that tells macOS to which preference domain the key-value pairs apply. As such, you'll usually find the payloadType relates to a payloadType.plist file somewhere in /Library/Managed Preferences (or `~/Library/Managed Preferences). Initially, I found it odd that the Defender client would be reading TWO separate preference domains. Also, neither article made mention of the other as a complete \u0026quot;story\u0026quot; for managing the client.\nDon't forget to grant Terminal.app permissions in the Security \u0026amp; Privacy preferences to validate the payloadType.plist in one of the two Managed Preferences locations.\nJamf and inTune Specifics The Deployment with a different MDM system article left a lot to be desired. I find that in this instance, the Jamf-specific deployment information provided the specific detail required for us to put together an appropriate plan. Much of the guidance in that article also applies to Workspace ONE UEM, but with some slightly different terminology:\nJamf Policies = Profiles in Workspace ONE Parlance In Workspace ONE UEM, you need only click Add \u0026gt; Profile in the top right corner, and build a macOS Device profile. While we typically suggest only a single payload per profile, in this instance, you need multiple payloads for a single app. Since none of the settings you'll be managing should conflict with other similar payloads, I would suggest creating a single profile with the following two payloads:\nThe Custom Settings payload for com.microsoft.wdav The Custom Settings payload for com.microsoft.wdav.atp I would then suggest creating separate profiles with each individual payload as follows (or add the information to a pre-existing profile containing the specific payload):\nThe Kernel Extension payload whitelisting the Team ID: UBF8T346G9 The Privacy Preferences payload granting com.microsoft.wdav the SystemPolicyAllFiles entitlement With regards to the Notification Settings payload, I recommend testing this setting with other apps in your environment generating notifications. In my experience, some apps in some versions of macOS have shown unintended behaviors when managing notification settings via MDM.\nAs you go to build the Custom Settings payloads, it's important to note that Workspace ONE UEM expects ONLY the payloadType dictionary (e.g. \u0026lt;dict\u0026gt;...\u0026lt;/dict\u0026gt;) and not the full mobileconfig profile with the Configuration wrapper. What this means is you'll need to trim down the content of the payload to make it work in Workspace ONE. I cover some of these basics in the Custom XML Payloads document on the EUC-Samples github repo.\nWhen adding multiple custom settings to a profile, use the plus sign (+) to add additional custom settings payloads. DO NOT paste multiple custom settings payloadType dictionaries (...) in the same text box (e.g. a single payload).\nJamf Packages = Internal Apps in Workspace ONE Parlance To deliver the wdav.pkg file to devices, you should parse it with the VMware AirWatch Admin Assistant app. You can then upload the package and plist as an internal macOS application. Next, paste the uninstall script in the uninstall script textbox for the Internal app.\nSummary In my experience, you can distill documentation meant for a different MDM or UEM vendor into a valid, workable solution for Workspace ONE. Also, when you find documentation that is confusing or incomplete, don't hesitate to file feedback!\n","link":"https://blog.terakedis.dev/2020-02-18-Deploying-Microsoft-ATP-Defender-for-Mac-using-Workspace-One/","section":"post","tags":["macOS"],"title":"Deploying Microsoft Defender ATP for macOS using Workspace ONE UEM"},{"body":"Working at VMware, I'm surrounded by great technology and super-smart folks! In our portfolio of technologies, the folks in our R\u0026amp;D have recently been putting quite a bit of effort into building out macOS capabilities for our Workspace ONE Tunnel client for macOS. Workspace ONE admins can leverage the same VMware technology they used to enable per-app VPN for iOS and Android, but now on macOS! There's a bit of nuance to configuring the VPN client if you're previously familiar with iOS (look for my Operational Tutorial soon to hit TechZone). That said, the premise is the same -- by configuring the appropriate rules, the Tunnel app redirects traffic from whitelisted applications back into your network through the Unified Access Gateway.\nSounds simple enough, right? So a few of us wondered if we could leverage some of this new technology with new Kerberos SSO Extension in macOS Catalina.\nUse-Cases Sitting down to plan out testing, there were four main use cases I hoped to prove out:\nWhat if the new Kerberos SSO Extension in macOS Catalina was just another one of those applications that you redirected over a VPN? Could you, in theory, get Kerberos Tickets to an unbound Mac to use for authenticating to Workspace ONE Access and/or internal websites (over Per-App VPN)? Could you also leverage the Kerberos SSO Extension to sync your local (non-mobile) macOS User account's password with the on-prem AD password over Per-App VPN? Could you change your on-prem AD password remotely over Per-App VPN when it neared the expiration date? It seemed pretty straightforward... but NOPE! I ran into quite a bit of unexpected behavior! Luckily, one of my coworkers, Adam, had a slightly different configuration than I and was able to test all of this as functioning for macOS that is on-network with on-premise Active Directory.\nTest Environment Configuration Here's how I have things configured in my test environment:\nIsolated Network with the following VMs: Two Windows Server 2016 Domain Controllers, one of which runs DNS Airwatch Cloud Connector and Workspace ONE Access Connnectors configured One IIS web server running two sites -- one configured for Kerberos Auth and one configured for Anonymous access. One ADCS server (not used for this testing) Unified Access Gateway Appliance v3.8 (the only VM with inbound access from the Internet) Tunnel/Edge service is enabled/configured SaaS-based Workspace ONE UEM and Workspace ONE Access. Workspace ONE Tunnel for macOS configured as an auto-deployed Volume Purchase app (from Apple Business Manager) The DNS name for my AD domain set up in Device Traffic Rules for tunneling. Google Chrome, Firefox, and Safari all configured to allow kerberos authentication to the website. Commands 1defaults write -g GSSDebugLevel 20 2 3defaults write -g KerberosDebugLevel 20 4 5log stream --debug --predicate \u0026#39;(subsystem == \u0026#34;com.apple.Heimdal\u0026#34;) OR (subsystem == \u0026#34;com.apple.AppSSO\u0026#34;) OR (subsystem == \u0026#34;org.h5l.gss\u0026#34;) OR (subsystem == \u0026#34;com.apple.network\u0026#34;) OR (process == \u0026#34;VMware Tunnel\u0026#34;) \u0026#39; Results As it turns out, the Kerberos SSO Extension in Catalina appears designed for situations where macOS is on-network with an on-premise Active Directory. I went through some testing using our Per-App Tunnel (and a full-device Global Protect VPN), and ran into the following testing results:\nTesting Item Per-App VPN (Tunnel) GlobalProtect (VPN) Kerberos Ticket Obtained over VPN Yes! Yes! Password Expiration Date Correct No! No! Extension Detects local PW different from AD No! Yes! User Change AD Password via Extension over VPN No! No! I was able to confirm that a kerberos ticket is obtained using klist in Terminal. I could also confirm that I could authenticate to the Kerberos-enabled IIS website without having to provide a username/password in the browser. Kerberos functionality appeared to be working, just not any other functionality.\nLooking Forward I used Apple's Feedback Assistant app to provide feedback to Apple and provided them with an environment to reproduce the issue. I'm hopeful that they can find a fix or provide some guidance to enabling the entirety of the Kerberos SSO Extension's functionality over a VPN.\n| Please reach out if you've done testing as well - I'd love to hear your experience!\nMore to come...\n","link":"https://blog.terakedis.dev/2020-02-04-macOS-Catalina-Kerberos-SSO-Over-VPN/","section":"post","tags":["macOS","SSO","VPN"],"title":"Testing macOS Catalina Kerberos SSO Extension Over VPN"},{"body":"","link":"https://blog.terakedis.dev/tags/jekyll/","section":"tags","tags":null,"title":"Jekyll"},{"body":"Welcome to my wholly reworked website! This time, I've dropped the complexity of Wordpress and opted for something significantly simpler: Jekyll Hugo and GitHub.\nHaving now hosted the site in GitHub Pages, here was the process I started with Hugo:\nCreated the Github Pages repo (rterakedis.github.io) -- this is where github pages looks for the blog's generated site files. Created a 2nd Github repo: rterakedis.github.io.hugo -- this repo holds the source files for Hugo to parse and generate the site. Added the GH Pages repo (rterakedis.github.io) as a submodule for rterakedis.github.io.hugo Edited the config.toml in the hugo files to include the following: baseURL = \u0026quot;https://terakedis.dev/\u0026quot; publishdir = \u0026quot;rterakedis.github.io\u0026quot; Add the CNAME file into the rterakedis.github.io repo and enable the custom name/https in the repo settings Ensure the output from hugo builds into the rterakedis.github.io directory in my local rterakedis.github.io.hugo directory Commit and push everything to GitHub A few quick links that I found particularly helpful when I was working with Jekyll:\nHow I'm using Jekyll in 2016 Secure \u0026amp; Fast Github pages with CloudFlare Utterances - Blog Commenting using GitHub Issues ","link":"https://blog.terakedis.dev/2018-06-15-welcome-to-jekyll/","section":"post","tags":["Jekyll","GitHub"],"title":"Welcome to Hugo (from Jekyll)"},{"body":"People ask me how to get better at their work - whether it's device management, automation, architecture, or just understanding why complex systems behave the way they do. The answer isn't usually \u0026quot;read the docs\u0026quot; (though that helps). It's about developing systems thinking. Here are resources that changed how I approach problems.\nSystems Thinking: Start with \u0026quot;The Goal\u0026quot; If you read one business book, read The Goal by Eliyahu Goldratt. It's a novel about manufacturing, but it's really about how to think about constraints, bottlenecks, and outcomes.\nThe core idea: Every system has a constraint. Find it. Optimizing everything else is pointless. This applies to IT as much as it does to factories.\nWhen you're troubleshooting, you're looking for the constraint. When you're designing systems, you're trying to avoid creating false constraints. When you're managing processes, you're identifying which step limits throughput. I've applied this thinking to MDM design (where's the enrollment bottleneck?), API integrations (what's the rate limit?), and feature adoption (what's stopping users from adopting this?). It's the difference between busy work and meaningful problem-solving.\nStart with process mapping. Here's the uncomfortable truth: what you think is the process is rarely what's actually happening. People skip steps, add workarounds, create \u0026quot;bandaids\u0026quot; that become permanent fixtures. That's why so many processes fall apart - you're applying constraints to an imaginary process, not the real one.\nBefore you fix anything, map the current state. Observe. Interview people doing the work. Document what's actually happening (not the flowchart from 2015). Then you can identify where the real constraints are and why those bandaids exist in the first place. Only then can you redesign for automation AND durability.\nRelated:\nThinking in Systems by Donella Meadows - denser, more scientific, but teaches you how to model complex systems.\nThe Scientific Method: Applying Rigor to Problem-Solving I have a degree in Integrated Life Sciences. That background taught me something that I've found invaluable in technology: hypothesis-driven thinking.\nThe scientific method isn't just for labs. It's how you should approach any problem:\nObserve - what's actually happening? (Not what you think is happening.) Hypothesize - form a testable explanation. Test - deliberately design an experiment to confirm or refute it. Iterate - adjust your hypothesis and repeat. This is why I push back on \u0026quot;quick fixes.\u0026quot; Quick fixes skip steps 1 and 3. You end up optimizing for the wrong constraint.\nIn practice:\nTroubleshooting a system? Form a hypothesis about why it's failing, then test it. Don't just make random changes. Building a new process? Prototype it with a small team first. Gather data. Iterate. Then scale. Designing a product feature? Get user feedback. Iterate. Don't ship version 1.0 and hope. Related reading:\nLean Product Development - iterative design applied to product Continuous Delivery by Jez Humble - how to apply this to software/infrastructure Iterative Planning: Especially with AI-Assisted Tools This is where a lot of people go wrong with AI copilots (GitHub Copilot, Claude, etc.). They jump straight to \u0026quot;build me a thing\u0026quot; and end up with something that partially works, then spend weeks unwinding and adjusting.\nStart with a plan. Before you ask an AI to code, design, or architect anything, you need:\nProblem statement - what's the real problem you're solving? (Not \u0026quot;I need a script.\u0026quot; But \u0026quot;I need to reduce manual provisioning of X by Y%.\u0026quot;) Definition of done - how will you know when it's complete? What does success look like? Key players and related systems - what systems does this touch? Who's affected? What constraints exist? Then iterate. Share your plan with the AI. It'll spot gaps. (\u0026quot;What about error handling?\u0026quot; \u0026quot;Have you considered this edge case?\u0026quot;) You'll discover scenarios you missed. Refine the plan. Then when you actually build it, you're working from clarity, not guessing.\nWhy this matters: A 30-minute planning conversation with an AI that identifies a missing piece is worth infinitely more than 20 hours of development that has to be reworked. The AI is excellent at asking \u0026quot;what about...\u0026quot; questions, but only if you give it the context to do so.\nIn practice:\nStart in a document (Markdown, Notion, whatever). Write the problem and success criteria. Share it with your AI copilot. Iterate. Add scenarios, edge cases, constraints. Once the plan is solid, then move to implementation. Keep iterating the plan as you build. Don't let implementation diverge from intent. Maintain a decision log. Every time you make a significant choice (why you chose architecture X over Y, why you changed the scope, why you added that constraint), log it with context. Future you will ask \u0026quot;why did we do it that way?\u0026quot; and you'll be grateful the answer is right there instead of lost in your brain. This applies whether you're building an integration, architecting a system, or designing a process. Plan → iterate → execute. The plan changes, but you're always working from a shared understanding instead of discovering requirements at 2 AM.\nUnderstanding Computing and Security Fundamentals You can't architect secure systems if you don't understand the fundamentals. Start with:\nPodcast (best for commutes):\nSecurity Now by Steve Gibson - weekly deep dives into security, cryptography, and how the internet works. Steve explains things clearly without dumbing them down. Highly recommended. Free resources:\nMalwarebytes Labs Blog - practical security explanations SANS Internet Storm Center - daily security summaries Troy Hunt's Blog - data breaches, authentication, HTTPS Books:\nThe Web Application Hacker's Handbook - if you build anything public-facing, understand attack vectors Cybersecurity and Cyberwar by Singer \u0026amp; Cole - big picture perspective AI and Large Language Models: Free Training Resources If you're curious about how LLMs work or want to learn prompt engineering:\nFree from Anthropic (Claude's maker):\nAnthropic's research papers and blog - technical but accessible. Start with explainers on constitutional AI and in-context learning. Claude's documentation - not just API docs; includes guides on prompt techniques and reasoning. Free courses and resources:\nDeepLearning.AI courses - short, free courses on LLMs, prompt engineering, and related topics Hugging Face's Course on NLP - free, rigorous intro to language models Papers with Code - research papers with working implementations Practical learning:\nUse LLMs on real problems. Get uncomfortable with prompt engineering. Try to break them. See what works and what doesn't. Read the papers behind the models you use. Understanding architecture (transformers, attention, token limits) helps you know what to ask for. Recommended listening:\nThe AI Podcast by Nvidia - interviews with researchers and practitioners. Good for understanding where the field is heading. The common thread Systems thinking, the scientific method, security fundamentals, AI - it all comes back to the same thing: understanding how things actually work, then thinking clearly about how to improve them.\nDon't just follow best practices. Understand why they're best practices. Then you'll know when to break them.\nComments welcome.\n","link":"https://blog.terakedis.dev/gettingstarted/","section":"page","tags":null,"title":"Getting Started: Learning Systems Thinking"},{"body":"","link":"https://blog.terakedis.dev/series/","section":"series","tags":null,"title":"Series"}]