in category:HomeLab program:Proxmox process:Script ~ read.

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:

  1. Cross-project SG rule updates (HomelabPveFwSgUpdate): sg_name=self.shorten_name(f"{self.site_name}_{self.env_name}_{firewall_sg_rule['app_name']}")
  2. Datacenter-wide SG creation: name=self.shorten_name(f"{self.site_name}_{self.env_name}_{self.app_name}") with comment=f"{self.site_name}_{self.env_name}_{self.app_name}"
  3. 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}") with comment=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}") with comment=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):

  1. Cross-project SG rule updates (HomelabPveFwSgUpdate via sg_name)
  2. Datacenter-wide SG creation (GroupLegacy via name, with human-readable name in comment)
  3. Per-container env-level SG reference (security_group=self.shorten_name(f"{self.env_name}"), with comment=self.env_name)
  4. Per-container service-level SG reference (security_group=self.shorten_name(f"{self.site_name}_{self.env_name}_{self.role_name}"), with comment)

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).