The Evolution of shorten_name: A Proxmox Firewall Naming Story
Proxmox has a hard limit on security group names: 18 characters. That's not a lot of room when your naming convention is hub_andrewcz-com_backend or curatingcollections-com. This is the story of how shorten_name came to be in homelab-pulumi, from the commit where it didn't exist at all to where it is today.
Before shorten_name (pre-October 2024)
In the early commits, firewall security group references in nix_pve_ct_cluster.py were plain string interpolation. There were two security groups referenced per container:
proxmoxve.network.FirewallRulesRuleArgs(
security_group=f"{self.env_name}_sg"
),
proxmoxve.network.FirewallRulesRuleArgs(
security_group="global_sg"
)
No datacenter-wide SG creation. No cross-project SG rule updates. No hashing. The env-level SG was just {env_name}_sg and there was a static global_sg. This worked fine when environment names were short.
October 3, 2024: shorten_name is born
Commit 5c16e7d ("Add new hardware, VLAN setup per env, exfra access, vaultwarden") is a big one. It introduces new hardware, per-environment VLANs, and along with that, the concept of cross-project firewall security group rules. Services need to be able to grant access to other services' security groups, and each service needs its own datacenter-wide security group that other services can add rules to.
This is where the 18-character limit becomes a real problem. Names like hub_andrewcz-com_vaultwarden are way over 18 chars. The solution: hash the name, use the hash as the SG name, and store the human-readable original in the SG's comment field.
The original implementation:
# The stupid thing will only let the firewall SG name be 18 characters long,
# so let's just hash it, and put the actual name in the comment.
def shorten_name(self, name: str) -> str:
# Create a SHA-1 hash of the name
hash_object = hashlib.sha1(name.encode())
# Get the binary digest of the hash
binary_dig = hash_object.digest()
# Encode using base32 to ensure alphanumeric characters only
base32_dig = base64.b32encode(binary_dig).decode('utf-8')
# Truncate to the first 18 characters
shortened_name = base32_dig[:18].lower()
return shortened_name
SHA-1 hash, base32 encode (to guarantee alphanumeric only), truncate to 18 chars, lowercase. Clean and deterministic. The function is used in three places at introduction:
- Cross-project SG rule updates (
HomelabPveFwSgUpdate):sg_name=self.shorten_name(f"{self.site_name}_{self.env_name}_{firewall_sg_rule['app_name']}") - Datacenter-wide SG creation:
name=self.shorten_name(f"{self.site_name}_{self.env_name}_{self.app_name}")withcomment=f"{self.site_name}_{self.env_name}_{self.app_name}" - Per-container firewall rule referencing the SG:
security_group=self.shorten_name(f"{self.site_name}_{self.env_name}_{self.app_name}")
The env-level SG ({env_name}_sg) and global_sg stay as plain strings for now.
October 6, 2024: The first-char fix
Three days later, commit 572bb4c ("Add audiobookshelf and managerserver") fixes a bug in shorten_name. The base32 alphabet is [A-Z2-7], which means the first character of the hash could be a digit (2-7). Proxmox requires SG names to start with a letter. The fix:
# Ensure the result matches the regex by prepending a letter if needed
# Check if the first character of the original name is a letter
first_char = base32_dig[0].lower() if re.match(r'[A-Za-z]', base32_dig[0]) else 'a'
# Truncate to fit within the 18-character limit
shortened_name = first_char + base32_dig[1:18].lower()
If the first base32 character is a letter, lowercase it. If it's a digit, substitute 'a'. Then take 17 more characters from the base32 digest. Total: 18 characters, guaranteed to start with a letter.
October 23, 2024: The ipset retreat
Commit 0e128f3 ("Get redis sentinel working") comments out the shorten_name usage for the ipset (IP set) name:
#name=self.shorten_name(f"{self.site_name}_{self.env_name}_{self.app_name}_ipset"),
#comment=f"{self.site_name}_{self.env_name}_{self.app_name}_ipset",
name=f"{self.site_name}_{self.env_name}_{self.app_name}_ipset",
The ipset name field apparently doesn't have the same 18-character constraint as security group names, so the raw name is used instead. The commented-out lines remain as a record of the earlier approach.
July 2025: curatingcollections-com breaks the env-level SG
This is where Kanboard ticket KB#7864 comes in. The ticket, created July 20, 2025, is titled "Implement hashing function for env-wide security group" with the description: "limited to 18 chars, and curatingcollections.com is longer than that."
The env-level security group reference ({env_name}_sg) had been a plain string this whole time. When curatingcollections-com gets added as an environment name, it blows past the 16-character limit for the security_group field in per-container firewall rules (different from the 18-char limit for SG names).
Commit 6fceada ("Add ghost and curatingcollections", July 30, 2025) adds a temporary fix with a TODO:
# TODO: the max len is 16, but `curatingcollections-com` is longer than that,
# so what we'd probably want to end up doing is to use the hashing
# function that we used previously to fix that, but I'm not going to
# implement that right now.
security_group=f"{self.env_name[:16]}_sg"
String slicing as a stopgap. curatingcollections-com truncated to 16 chars becomes curatingcollecti_sg. Not great, but it works for now.
September 2, 2025: The TODO is resolved
Commit 0e553ba ("Restructuring a lot of things") is where KB#7864 gets resolved. This commit does a major restructuring: app_name is replaced with role_name throughout the codebase (separating the concept of roles from apps), and the env-level SG TODO is finally addressed:
# The max len is 16, but `curatingcollections-com` is longer than that,
# so what we'd probably want to end up doing is to use the hashing
# function that we used previously to fix that.
security_group=self.shorten_name(f"{self.env_name}"),
comment=self.env_name
The TODO prefix is dropped (it's no longer a TODO, it's done), and f"{self.env_name[:16]}_sg" is replaced with self.shorten_name(f"{self.env_name}"). The human-readable env name goes in the comment field, same pattern as the service-level SG.
All shorten_name call sites are also updated from app_name to role_name:
sg_name=self.shorten_name(f"{self.site_name}_{self.env_name}_{firewall_sg_rule['role_name']}")name=self.shorten_name(f"{self.site_name}_{self.env_name}_{self.role_name}")withcomment=f"{self.site_name}_{self.env_name}_{self.role_name}"security_group=self.shorten_name(f"{self.site_name}_{self.env_name}_{self.role_name}")withcomment=f"{self.site_name}_{self.env_name}_{self.role_name}"
June 25, 2026: A debug log gets commented out
Commit 036280d ("nixbuild: dedicated build server, scaffold-cache substituter refactor, litestream overlay") is the most recent commit to touch shorten_name. The only change to the function is a debug log line being commented out:
# pulumi.info(f"shorten_name: '{name}' -> '{shortened_name}'")
The function itself is unchanged. This is just cleanup from a debugging session.
Where we are today
The current shorten_name implementation in nix_pve_ct_cluster.py:
# The stupid thing will only let the firewall SG name be 18 characters long,
# so let's just hash it, and put the actual name in the comment.
def shorten_name(self, name: str) -> str:
# Create a SHA-1 hash of the name
hash_object = hashlib.sha1(name.encode())
# Get the binary digest of the hash
binary_dig = hash_object.digest()
# Encode using base32 to ensure alphanumeric characters only
base32_dig = base64.b32encode(binary_dig).decode("utf-8")
# Ensure the result matches the regex by prepending a letter if needed
# Check if the first character of the original name is a letter
first_char = (
base32_dig[0].lower() if re.match(r"[A-Za-z]", base32_dig[0]) else "a"
)
# Truncate to fit within the 18-character limit
shortened_name = first_char + base32_dig[1:18].lower()
# pulumi.info(f"shorten_name: '{name}' -> '{shortened_name}'")
return shortened_name
It's used in four active call sites (one commented-out ipset usage remains as a record):
- Cross-project SG rule updates (
HomelabPveFwSgUpdateviasg_name) - Datacenter-wide SG creation (
GroupLegacyvianame, with human-readable name incomment) - Per-container env-level SG reference (
security_group=self.shorten_name(f"{self.env_name}"), withcomment=self.env_name) - Per-container service-level SG reference (
security_group=self.shorten_name(f"{self.site_name}_{self.env_name}_{self.role_name}"), withcomment)
The function is an instance method on HomelabNixPveCtCluster but uses no instance state; it's a pure function of its name argument. There are no tests for it. The 18-character output length is hardcoded, not parameterized. The pattern is consistent across all call sites: hash the name for the Proxmox field, store the human-readable original in the comment field.
KB#7864 is currently in Review status, in the Tech Debt swimlane. It has no comments. The work it describes was done across two commits: the initial shorten_name introduction in October 2024 (which handled service-level SGs) and the September 2025 restructuring (which extended it to the env-level SG that curatingcollections-com had broken).